mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-29 04:49:31 +00:00
Compare commits
13 Commits
hanif/cli-
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9c415fc41 | ||
|
|
1e392e4437 | ||
|
|
4c4eb9a94e | ||
|
|
83a00843a0 | ||
|
|
991c36d649 | ||
|
|
f6f68573b6 | ||
|
|
28d0d218ff | ||
|
|
d99f4c8164 | ||
|
|
ddd62e25df | ||
|
|
35970f61dc | ||
|
|
2169b72c94 | ||
|
|
0cb56b3cfa | ||
|
|
81b06d68f1 |
@@ -1793,9 +1793,8 @@ int CLI::run(int argc, char **argv)
|
||||
old_printable_area = config.option<ConfigOptionPoints>("printable_area", true)->values;
|
||||
old_exclude_area = config.option<ConfigOptionPoints>("bed_exclude_area", true)->values;
|
||||
if (old_printable_area.size() >= 4) {
|
||||
BoundingBoxf old_printable_bbox(old_printable_area);
|
||||
old_printable_width = static_cast<int>(old_printable_bbox.size().x());
|
||||
old_printable_depth = static_cast<int>(old_printable_bbox.size().y());
|
||||
old_printable_width = (int)(old_printable_area[2].x() - old_printable_area[0].x());
|
||||
old_printable_depth = (int)(old_printable_area[2].y() - old_printable_area[0].y());
|
||||
}
|
||||
old_printable_height = (int)(config.opt_float("printable_height"));
|
||||
|
||||
@@ -2344,9 +2343,8 @@ int CLI::run(int argc, char **argv)
|
||||
Pointfs orig_printable_area;
|
||||
orig_printable_area = config.option<ConfigOptionPoints>("printable_area", true)->values;
|
||||
if (orig_printable_area.size() >= 4) {
|
||||
BoundingBoxf orig_printable_bbox(orig_printable_area);
|
||||
orig_printable_width = static_cast<int>(orig_printable_bbox.size().x());
|
||||
orig_printable_depth = static_cast<int>(orig_printable_bbox.size().y());
|
||||
orig_printable_width = (int)(orig_printable_area[2].x() - orig_printable_area[0].x());
|
||||
orig_printable_depth = (int)(orig_printable_area[2].y() - orig_printable_area[0].y());
|
||||
}
|
||||
orig_printable_height = (int)(config.opt_float("printable_height"));
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(":%1%, check printable size: old_printable_width=%2%, orig_printable_width=%3%, old_printable_depth=%4%, orig_printable_depth=%5%, old_printable_height=%6%, orig_printable_height=%7%")
|
||||
@@ -3140,22 +3138,7 @@ int CLI::run(int argc, char **argv)
|
||||
std::vector<int> old_variant_counts(filament_count, 1), new_variant_counts;
|
||||
|
||||
ConfigOptionInts* filament_self_index_opt = m_print_config.option<ConfigOptionInts>("filament_self_index");
|
||||
bool need_regenerate_self_index = !filament_self_index_opt;
|
||||
if (filament_self_index_opt) {
|
||||
// a filament_self_index carried over from a project with a different
|
||||
// filament_count can imply more distinct filament groups than currently exist.
|
||||
// old_start_indice/old_variant_counts below are sized to filament_count, so an
|
||||
// unreconciled index walks old_start_indice[++k] past its bounds (heap corruption).
|
||||
int max_self_index = 0;
|
||||
for (int v : filament_self_index_opt->values)
|
||||
max_self_index = std::max(max_self_index, v);
|
||||
if (max_self_index > filament_count) {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index implies %1% filament groups but filament_count is %2%, regenerating")
|
||||
% max_self_index % filament_count;
|
||||
need_regenerate_self_index = true;
|
||||
}
|
||||
}
|
||||
if (need_regenerate_self_index) {
|
||||
if (!filament_self_index_opt) {
|
||||
filament_self_index_opt = m_print_config.option<ConfigOptionInts>("filament_self_index", true);
|
||||
std::vector<int>& filament_self_indice = filament_self_index_opt->values;
|
||||
filament_self_indice.resize(filament_count);
|
||||
@@ -3749,8 +3732,6 @@ int CLI::run(int argc, char **argv)
|
||||
double height_to_lid = m_print_config.opt_float("extruder_clearance_height_to_lid");
|
||||
double height_to_rod = m_print_config.opt_float("extruder_clearance_height_to_rod");
|
||||
double clearance_radius = m_print_config.opt_float("extruder_clearance_radius");
|
||||
double nozzle_height = m_print_config.opt_float("nozzle_height");
|
||||
Vec2d align_center = m_print_config.option<ConfigOptionPoint>("best_object_pos")->value;
|
||||
int shared_printable_width = 0, shared_printable_depth = 0, shared_printable_height = 0, shared_center_x = 0, shared_center_y = 0;
|
||||
//double plate_stride;
|
||||
std::string bed_texture;
|
||||
@@ -3761,11 +3742,8 @@ int CLI::run(int argc, char **argv)
|
||||
if (m_print_config.opt<ConfigOptionFloatsNullable>("extruder_printable_height")) {
|
||||
current_extruder_print_heights = m_print_config.opt<ConfigOptionFloatsNullable>("extruder_printable_height")->values;
|
||||
}
|
||||
{
|
||||
BoundingBoxf current_printable_bbox(current_printable_area);
|
||||
current_printable_width = static_cast<int>(current_printable_bbox.size().x());
|
||||
current_printable_depth = static_cast<int>(current_printable_bbox.size().y());
|
||||
}
|
||||
current_printable_width = current_printable_area[2].x() - current_printable_area[0].x();
|
||||
current_printable_depth = current_printable_area[2].y() - current_printable_area[0].y();
|
||||
current_printable_height = print_height;
|
||||
if (old_printable_width == 0)
|
||||
old_printable_width = current_printable_width;
|
||||
@@ -3966,13 +3944,6 @@ int CLI::run(int argc, char **argv)
|
||||
ConfigOptionFloats *wipe_x_option = dynamic_cast<ConfigOptionFloats *>(print_config.option("wipe_tower_x"));
|
||||
ConfigOptionFloats *wipe_y_option = dynamic_cast<ConfigOptionFloats *>(print_config.option("wipe_tower_y"));
|
||||
|
||||
// get_at() clamps an out-of-range index to entry 0 instead of erroring, which
|
||||
// would silently reuse another plate's wipe tower position here. Warn so a mismatched
|
||||
// wipe_tower_x/y array (e.g. from a project saved before this plate was added) is visible.
|
||||
if (static_cast<size_t>(plate_index) >= wipe_x_option->values.size() || static_cast<size_t>(plate_index) >= wipe_y_option->values.size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("plate %1%: wipe_tower_x/y only has %2%/%3% entries, reusing entry 0's position")
|
||||
%(plate_index+1) %wipe_x_option->values.size() %wipe_y_option->values.size();
|
||||
}
|
||||
plate_obj_size_info.wipe_x = wipe_x_option->get_at(plate_index);
|
||||
plate_obj_size_info.wipe_y = wipe_y_option->get_at(plate_index);
|
||||
|
||||
@@ -4168,9 +4139,8 @@ int CLI::run(int argc, char **argv)
|
||||
temp_extruder_print_heights = config.option<ConfigOptionFloatsNullable>("extruder_printable_height", true)->values;
|
||||
|
||||
if (temp_printable_area.size() >= 4) {
|
||||
BoundingBoxf temp_printable_bbox(temp_printable_area);
|
||||
printer_plate.printable_width = static_cast<int>(temp_printable_bbox.size().x());
|
||||
printer_plate.printable_depth = static_cast<int>(temp_printable_bbox.size().y());
|
||||
printer_plate.printable_width = (int)(temp_printable_area[2].x() - temp_printable_area[0].x());
|
||||
printer_plate.printable_depth = (int)(temp_printable_area[2].y() - temp_printable_area[0].y());
|
||||
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
|
||||
}
|
||||
if (temp_exclude_area.size() >= 4) {
|
||||
@@ -4818,8 +4788,6 @@ int CLI::run(int argc, char **argv)
|
||||
arrange_cfg.clearance_height_to_rod = height_to_rod;
|
||||
arrange_cfg.clearance_height_to_lid = height_to_lid;
|
||||
arrange_cfg.clearance_radius = clearance_radius;
|
||||
arrange_cfg.nozzle_height = nozzle_height;
|
||||
arrange_cfg.align_center = align_center;
|
||||
arrange_cfg.printable_height = print_height;
|
||||
arrange_cfg.min_obj_distance = 0;
|
||||
if (arrange_cfg.is_seq_print) {
|
||||
@@ -5270,8 +5238,6 @@ int CLI::run(int argc, char **argv)
|
||||
arrange_cfg.clearance_height_to_rod = height_to_rod;
|
||||
arrange_cfg.clearance_height_to_lid = height_to_lid;
|
||||
arrange_cfg.clearance_radius = clearance_radius;
|
||||
arrange_cfg.nozzle_height = nozzle_height;
|
||||
arrange_cfg.align_center = align_center;
|
||||
arrange_cfg.printable_height = print_height;
|
||||
arrange_cfg.min_obj_distance = 0;
|
||||
if (arrange_cfg.is_seq_print) {
|
||||
@@ -5563,26 +5529,6 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
finished_arrange = true;
|
||||
}
|
||||
// CLI has no m_plater, so PartPlateList::create_plate() never backfills
|
||||
// wipe_tower_x/y for plates created here during arrange overflow (unlike GUI's
|
||||
// set_default_wipe_tower_pos_for_plate()). Keep both arrays sized to the actual
|
||||
// plate count so a later per-plate get_at() never silently reuses another plate's
|
||||
// wipe tower position via ConfigOptionVector's out-of-range clamp.
|
||||
{
|
||||
int final_plate_count = partplate_list.get_plate_count();
|
||||
ConfigOptionFloats* wipe_x_opt = m_print_config.option<ConfigOptionFloats>("wipe_tower_x");
|
||||
ConfigOptionFloats* wipe_y_opt = m_print_config.option<ConfigOptionFloats>("wipe_tower_y");
|
||||
if (wipe_x_opt && !wipe_x_opt->values.empty() && wipe_x_opt->values.size() < static_cast<size_t>(final_plate_count)) {
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("wipe_tower_x had %1% entries for %2% plates, backfilling with entry 0")
|
||||
% wipe_x_opt->values.size() % final_plate_count;
|
||||
wipe_x_opt->values.resize(final_plate_count, wipe_x_opt->values.front());
|
||||
}
|
||||
if (wipe_y_opt && !wipe_y_opt->values.empty() && wipe_y_opt->values.size() < static_cast<size_t>(final_plate_count)) {
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("wipe_tower_y had %1% entries for %2% plates, backfilling with entry 0")
|
||||
% wipe_y_opt->values.size() % final_plate_count;
|
||||
wipe_y_opt->values.resize(final_plate_count, wipe_y_opt->values.front());
|
||||
}
|
||||
}
|
||||
original_model.clear_objects();
|
||||
original_model.clear_materials();
|
||||
}
|
||||
@@ -6570,20 +6516,10 @@ int CLI::run(int argc, char **argv)
|
||||
plate_data->nozzle_diameters = nozzle_diameter_str;
|
||||
|
||||
for (auto it = plate_data->slice_filaments_info.begin(); it != plate_data->slice_filaments_info.end(); it++) {
|
||||
// ConfigOptionVector::get_at() falls back to values.front() when the index is out of
|
||||
// range, but that is undefined behavior when values is empty outright (e.g. filament_ids
|
||||
// is never populated on a from-scratch slice with no --load-filaments) - guard every
|
||||
// get_at() here on the vector actually having an entry at it->id before calling it.
|
||||
bool valid_id = it->id >= 0;
|
||||
std::string display_filament_type;
|
||||
if (valid_id && filament_types && static_cast<size_t>(it->id) < filament_types->values.size())
|
||||
it->type = m_print_config.get_filament_type(display_filament_type, it->id);
|
||||
it->color = (valid_id && filament_color && static_cast<size_t>(it->id) < filament_color->values.size()) ?
|
||||
filament_color->get_at(it->id) :
|
||||
"#FFFFFF";
|
||||
it->filament_id = (valid_id && filament_id && static_cast<size_t>(it->id) < filament_id->values.size()) ?
|
||||
filament_id->get_at(it->id) :
|
||||
"";
|
||||
it->type = m_print_config.get_filament_type(display_filament_type, it->id);
|
||||
it->color = filament_color ? filament_color->get_at(it->id) : "#FFFFFF";
|
||||
it->filament_id = filament_id?filament_id->get_at(it->id):"";
|
||||
}
|
||||
|
||||
if (!plate_data->plate_thumbnail.is_valid()) {
|
||||
@@ -7375,10 +7311,6 @@ bool CLI::setup(int argc, char **argv)
|
||||
m_config.option(optdef.first, true);
|
||||
|
||||
set_data_dir(m_config.opt_string("datadir"));
|
||||
if (!data_dir().empty() && !boost::filesystem::exists(data_dir())) {
|
||||
boost::nowide::cerr << "Could not create data directory: " << data_dir() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
//FIXME Validating at this stage most likely does not make sense, as the config is not fully initialized yet.
|
||||
if (!validity.empty()) {
|
||||
@@ -7452,7 +7384,7 @@ void CLI::print_help(bool include_print_options, PrinterTechnology printer_techn
|
||||
<< std::endl
|
||||
<< "Print setting priorities:" << std::endl
|
||||
<< "\t1) setting values from the command line (highest priority)"<< std::endl
|
||||
<< "\t2) setting values loaded with --load-settings and --load-filaments" << std::endl
|
||||
<< "\t2) setting values loaded with --load_settings and --load_filaments" << std::endl
|
||||
<< "\t3) setting values loaded from 3mf(lowest priority)" << std::endl;
|
||||
|
||||
/*if (include_print_options) {
|
||||
@@ -7491,10 +7423,6 @@ bool CLI::export_models(IO::ExportFormat format, std::string path_dir)
|
||||
for (ModelObject* model_object : model.objects)
|
||||
{
|
||||
const std::string path = this->output_filepath(*model_object, index++, format, path_dir);
|
||||
if (path.empty()) {
|
||||
boost::nowide::cerr << "Could not create output directory for STL export" << std::endl;
|
||||
return false;
|
||||
}
|
||||
success = Slic3r::store_stl(path.c_str(), model_object, true);
|
||||
if (success)
|
||||
BOOST_LOG_TRIVIAL(info) << "Model successfully exported to " << path << std::endl;
|
||||
@@ -7620,19 +7548,8 @@ std::string CLI::output_filepath(const ModelObject &object, unsigned int index,
|
||||
output_path = subdir + "/"+file_name;
|
||||
|
||||
boost::filesystem::path subdir_path(subdir);
|
||||
if (!boost::filesystem::exists(subdir_path)) {
|
||||
try {
|
||||
boost::filesystem::create_directories(subdir_path);
|
||||
} catch (const boost::filesystem::filesystem_error &ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to create output directory " << subdir_path.string() << ": " << ex.what();
|
||||
}
|
||||
if (!boost::filesystem::exists(subdir_path)) {
|
||||
// Directory creation failed and won't succeed on a retry (same path, same cause) -
|
||||
// signal failure now instead of letting every object in the model repeat the same
|
||||
// doomed attempt and fail with a less specific "export failed" error later.
|
||||
return std::string();
|
||||
}
|
||||
}
|
||||
if (!boost::filesystem::exists(subdir_path))
|
||||
boost::filesystem::create_directory(subdir_path);
|
||||
return output_path;
|
||||
}
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ public:
|
||||
m_bbox(bbox.min - Point(SCALED_EPSILON, SCALED_EPSILON), bbox.max + Point(SCALED_EPSILON, SCALED_EPSILON)) {}
|
||||
size_t idx() const { return m_idx; }
|
||||
const BoundingBox& bbox() const { return m_bbox; }
|
||||
Point centroid() const { return (m_bbox.min() + m_bbox.max()) / 2; }
|
||||
Point centroid() const { return (m_bbox.min() + m_bbox.max() / 2); }
|
||||
private:
|
||||
size_t m_idx;
|
||||
BoundingBox m_bbox;
|
||||
|
||||
@@ -13,6 +13,7 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
|
||||
: m_bed_shape(printable_area), m_max_print_height(printable_height), m_extruder_shapes(extruder_areas), m_extruder_printable_height(extruder_printable_heights)
|
||||
{
|
||||
assert(printable_height >= 0);
|
||||
//assert(extruder_printable_heights.size() == extruder_areas.size());
|
||||
|
||||
m_polygon = Polygon::new_scale(printable_area);
|
||||
assert(m_polygon.is_counter_clockwise());
|
||||
@@ -99,12 +100,7 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
|
||||
return;
|
||||
}
|
||||
|
||||
if (index >= extruder_printable_heights.size())
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("extruder_printable_height has only %1% entries but extruder_printable_area has %2%; using bed printable_height for extruder %3%")
|
||||
% extruder_printable_heights.size() % m_extruder_shapes.size() % index;
|
||||
const double extruder_height = index < extruder_printable_heights.size() ? extruder_printable_heights[index] : printable_height;
|
||||
|
||||
if ((extruder_shape == printable_area)&&(extruder_height == printable_height)) {
|
||||
if ((extruder_shape == printable_area)&&(extruder_printable_heights[index] == printable_height)) {
|
||||
extruder_volume.same_with_bed = true;
|
||||
extruder_volume.type = m_type;
|
||||
extruder_volume.bbox = m_bbox;
|
||||
@@ -117,7 +113,7 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
|
||||
double poly_area = poly.area();
|
||||
extruder_volume.bbox = get_extents(poly);
|
||||
BoundingBoxf temp_bboxf = get_extents(extruder_shape);
|
||||
extruder_volume.bboxf = BoundingBoxf3{ to_3d(temp_bboxf.min, 0.), to_3d(temp_bboxf.max, extruder_height) };
|
||||
extruder_volume.bboxf = BoundingBoxf3{ to_3d(temp_bboxf.min, 0.), to_3d(temp_bboxf.max, extruder_printable_heights[index]) };
|
||||
|
||||
if (extruder_shape.size() >= 4 && std::abs((poly_area - double(extruder_volume.bbox.size().x()) * double(extruder_volume.bbox.size().y()))) < sqr(SCALED_EPSILON))
|
||||
{
|
||||
|
||||
@@ -1812,31 +1812,17 @@ bool DynamicConfig::read_cli(int argc, const char* const argv[], t_config_option
|
||||
// to the end of the value.
|
||||
if (opt_base->type() == coBools && value.empty())
|
||||
static_cast<ConfigOptionBools*>(opt_base)->values.push_back(!no);
|
||||
else {
|
||||
else
|
||||
// Deserialize any other vector value (ConfigOptionInts, Floats, Percents, Points) the same way
|
||||
// they get deserialized from an .ini file. For ConfigOptionStrings, that means that the C-style unescape
|
||||
// will be applied for values enclosed in quotes, while values non-enclosed in quotes are left to be
|
||||
// unescaped by the calling shell.
|
||||
bool deserialized = false;
|
||||
try {
|
||||
deserialized = opt_vector->deserialize(value, true);
|
||||
} catch (const std::exception &ex) {
|
||||
// e.g. "nil" deserialized into a non-nullable vector option throws instead of
|
||||
// returning false - treat that the same as any other invalid value here.
|
||||
deserialized = false;
|
||||
}
|
||||
if (! deserialized) {
|
||||
boost::nowide::cerr << "Invalid value for option --" << token.c_str() << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
opt_vector->deserialize(value, true);
|
||||
} else if (opt_base->type() == coBool) {
|
||||
if (value.empty())
|
||||
static_cast<ConfigOptionBool*>(opt_base)->value = !no;
|
||||
else if (! opt_base->deserialize(value)) {
|
||||
boost::nowide::cerr << "Invalid value for option --" << token.c_str() << std::endl;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
opt_base->deserialize(value);
|
||||
} else if (opt_base->type() == coString) {
|
||||
// Do not unescape single string values, the unescaping is left to the calling shell.
|
||||
static_cast<ConfigOptionString*>(opt_base)->value = value;
|
||||
|
||||
@@ -55,9 +55,9 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
|
||||
boost::filesystem::path temp_mtl_path(mtl_file);
|
||||
mtl_path = temp_mtl_path;
|
||||
}
|
||||
const std::string _mtl_path = (mtl_name_is_path ? mtl_abs_path : mtl_path).string();
|
||||
auto _mtl_path = mtl_name_is_path ? mtl_abs_path.string().c_str() : mtl_path.string().c_str();
|
||||
if (boost::filesystem::exists(mtl_name_is_path ? mtl_abs_path : mtl_path)) {
|
||||
if (!ObjParser::mtlparse(_mtl_path.c_str(), mtl_data)) {
|
||||
if (!ObjParser::mtlparse(_mtl_path, mtl_data)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "load_obj:load_mtl: failed to parse " << _mtl_path;
|
||||
message = _L("load mtl in obj: failed to parse");
|
||||
return false;
|
||||
|
||||
@@ -111,19 +111,14 @@ bool StepPreProcessor::isUtf8File(const char* path)
|
||||
bool StepPreProcessor::isUtf8(const std::string str)
|
||||
{
|
||||
size_t num = 0;
|
||||
size_t i = 0;
|
||||
int i = 0;
|
||||
while (i < str.length()) {
|
||||
const unsigned char lead = static_cast<unsigned char>(str[i]);
|
||||
if ((lead & 0x80) == 0x00) {
|
||||
if ((str[i] & 0x80) == 0x00) {
|
||||
i++;
|
||||
// preNum() counts the leading 1 bits, and a multi-byte sequence is 2 to 4
|
||||
// bytes long, so anything outside that range is not a lead byte.
|
||||
} else if ((num = preNum(lead)) >= 2 && num <= 4) {
|
||||
if (i + num > str.length())
|
||||
return false;
|
||||
} else if ((num = preNum(str[i])) > 2) {
|
||||
i++;
|
||||
for (size_t j = 0; j < num - 1; j++) {
|
||||
if ((static_cast<unsigned char>(str[i]) & 0xc0) != 0x80)
|
||||
for (int j = 0; j < num - 1; j++) {
|
||||
if ((str[i] & 0xc0) != 0x80)
|
||||
return false;
|
||||
i++;
|
||||
}
|
||||
@@ -137,20 +132,15 @@ bool StepPreProcessor::isUtf8(const std::string str)
|
||||
bool StepPreProcessor::isGBK(const std::string str) {
|
||||
size_t i = 0;
|
||||
while (i < str.length()) {
|
||||
// char is signed here, so every byte compares <= 0x7f unless widened first.
|
||||
const unsigned char lead = static_cast<unsigned char>(str[i]);
|
||||
if (lead <= 0x7f) {
|
||||
if (str[i] <= 0x7f) {
|
||||
i++;
|
||||
continue;
|
||||
} else {
|
||||
if (i + 1 >= str.length())
|
||||
return false;
|
||||
const unsigned char trail = static_cast<unsigned char>(str[i + 1]);
|
||||
if (lead >= 0x81 &&
|
||||
lead <= 0xfe &&
|
||||
trail >= 0x40 &&
|
||||
trail <= 0xfe &&
|
||||
trail != 0xf7) {
|
||||
if (str[i] >= 0x81 &&
|
||||
str[i] <= 0xfe &&
|
||||
str[i + 1] >= 0x40 &&
|
||||
str[i + 1] <= 0xfe &&
|
||||
str[i + 1] != 0xf7) {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -11948,11 +11948,13 @@ CLIActionsConfigDef::CLIActionsConfigDef()
|
||||
def = this->add("load_defaultfila", coBool);
|
||||
def->label = L("Load default filaments");
|
||||
def->tooltip = L("Load first filament as default for those not loaded.");
|
||||
def->cli_params = "option";
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("min_save", coBool);
|
||||
def->label = L("Minimum save");
|
||||
def->tooltip = L("Export 3MF with minimum size.");
|
||||
def->cli_params = "option";
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("mtcpp", coInt);
|
||||
@@ -11978,6 +11980,7 @@ CLIActionsConfigDef::CLIActionsConfigDef()
|
||||
def = this->add("normative_check", coBool);
|
||||
def->label = L("Normative check");
|
||||
def->tooltip = L("Check the normative items.");
|
||||
def->cli_params = "option";
|
||||
def->set_default_value(new ConfigOptionBool(true));
|
||||
|
||||
/*def = this->add("help_fff", coBool);
|
||||
@@ -12244,7 +12247,7 @@ CLIMiscConfigDef::CLIMiscConfigDef()
|
||||
def->cli_params = "level";
|
||||
def->set_default_value(new ConfigOptionInt(1));
|
||||
|
||||
def = this->add("logfile", coString);
|
||||
def = this->add("logfile", coInt);
|
||||
def->label = L("Log file");
|
||||
def->tooltip = L("Redirects debug logging to file.\n");
|
||||
def->cli_params = "file";
|
||||
@@ -12292,6 +12295,7 @@ CLIMiscConfigDef::CLIMiscConfigDef()
|
||||
def = this->add("skip_modified_gcodes", coBool);
|
||||
def->label = L("Skip modified G-code in 3MF");
|
||||
def->tooltip = L("Skip the modified G-code in 3MF from printer or filament presets.");
|
||||
def->cli_params = "option";
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("makerlab_name", coString);
|
||||
@@ -12321,12 +12325,14 @@ CLIMiscConfigDef::CLIMiscConfigDef()
|
||||
def = this->add("allow_newer_file", coBool);
|
||||
def->label = L("Allow 3MF with newer version to be sliced");
|
||||
def->tooltip = L("Allow 3MF with newer version to be sliced.");
|
||||
def->cli_params = "option";
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("allow_mix_temp", coBool);
|
||||
// internal use only, don't need translation
|
||||
def->label = "Allow filaments with high/low temperature to be printed together";
|
||||
def->tooltip = "Allow filaments with high/low temperature to be printed together.";
|
||||
def->cli_params = "option";
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
}
|
||||
|
||||
|
||||
@@ -310,11 +310,7 @@ void set_data_dir(const std::string &dir)
|
||||
{
|
||||
g_data_dir = dir;
|
||||
if (!g_data_dir.empty() && !boost::filesystem::exists(g_data_dir)) {
|
||||
try {
|
||||
boost::filesystem::create_directories(g_data_dir);
|
||||
} catch (const boost::filesystem::filesystem_error &ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "set_data_dir: failed to create data directory " << g_data_dir << ": " << ex.what();
|
||||
}
|
||||
boost::filesystem::create_directory(g_data_dir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1757,25 +1757,26 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D
|
||||
else
|
||||
obj_support = glb_support;
|
||||
|
||||
if (obj_support) {
|
||||
int obj_support_intf_extr = 0;
|
||||
const ConfigOption* support_intf_extr_opt = object->config.option("support_interface_filament");
|
||||
if (support_intf_extr_opt != nullptr)
|
||||
obj_support_intf_extr = support_intf_extr_opt->getInt();
|
||||
if (obj_support_intf_extr != 0)
|
||||
plate_extruders.push_back(obj_support_intf_extr);
|
||||
else if (glb_support_intf_extr != 0)
|
||||
plate_extruders.push_back(glb_support_intf_extr);
|
||||
if (!obj_support)
|
||||
continue;
|
||||
|
||||
int obj_support_extr = 0;
|
||||
const ConfigOption* support_extr_opt = object->config.option("support_filament");
|
||||
if (support_extr_opt != nullptr)
|
||||
obj_support_extr = support_extr_opt->getInt();
|
||||
if (obj_support_extr != 0)
|
||||
plate_extruders.push_back(obj_support_extr);
|
||||
else if (glb_support_extr != 0)
|
||||
plate_extruders.push_back(glb_support_extr);
|
||||
}
|
||||
int obj_support_intf_extr = 0;
|
||||
const ConfigOption* support_intf_extr_opt = object->config.option("support_interface_filament");
|
||||
if (support_intf_extr_opt != nullptr)
|
||||
obj_support_intf_extr = support_intf_extr_opt->getInt();
|
||||
if (obj_support_intf_extr != 0)
|
||||
plate_extruders.push_back(obj_support_intf_extr);
|
||||
else if (glb_support_intf_extr != 0)
|
||||
plate_extruders.push_back(glb_support_intf_extr);
|
||||
|
||||
int obj_support_extr = 0;
|
||||
const ConfigOption* support_extr_opt = object->config.option("support_filament");
|
||||
if (support_extr_opt != nullptr)
|
||||
obj_support_extr = support_extr_opt->getInt();
|
||||
if (obj_support_extr != 0)
|
||||
plate_extruders.push_back(obj_support_extr);
|
||||
else if (glb_support_extr != 0)
|
||||
plate_extruders.push_back(glb_support_extr);
|
||||
|
||||
int obj_outer_wall_extr = 0;
|
||||
if (const ConfigOption* wall_opt = object->config.option("outer_wall_filament_id"); wall_opt != nullptr)
|
||||
@@ -2770,16 +2771,19 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi
|
||||
plate_box.min.z() += instance_box.min.z(); // not considering outsize if sinking
|
||||
|
||||
if (instance_box.min.z() < SINKING_Z_THRESHOLD) {
|
||||
// For sinking object, we use a more expensive algorithm so part below build plate won't be considered.
|
||||
// m_height mirrors the printer's printable height independent of m_plater, so this runs in CLI too.
|
||||
if (plate_box.intersects(instance_box)) {
|
||||
// TODO: FIXME: this does not take exclusion area into account
|
||||
const BuildVolume build_volume(get_shape(), m_height, m_extruder_areas, m_extruder_heights);
|
||||
const auto state = instance->calc_print_volume_state(build_volume);
|
||||
outside = state == ModelInstancePVS_Partly_Outside;
|
||||
}
|
||||
} else if (plate_box.contains(instance_box)) {
|
||||
if (m_exclude_bounding_box.size() > 0)
|
||||
// Orca: For sinking object, we use a more expensive algorithm so part below build plate won't be considered
|
||||
// m_plater is null in CLI mode.
|
||||
if (m_plater && plate_box.intersects(instance_box)) {
|
||||
// TODO: FIXME: this does not take exclusion area into account
|
||||
const BuildVolume build_volume(get_shape(), m_plater->build_volume().printable_height(), m_extruder_areas, m_extruder_heights);
|
||||
const auto state = instance->calc_print_volume_state(build_volume);
|
||||
outside = state == ModelInstancePVS_Partly_Outside;
|
||||
}
|
||||
}
|
||||
else
|
||||
if (plate_box.contains(instance_box))
|
||||
{
|
||||
if (m_exclude_bounding_box.size() > 0)
|
||||
{
|
||||
Polygon hull = instance->convex_hull_2d();
|
||||
int index;
|
||||
@@ -2797,9 +2801,9 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi
|
||||
}
|
||||
else
|
||||
outside = false;
|
||||
}
|
||||
}
|
||||
|
||||
return outside;
|
||||
return outside;
|
||||
}
|
||||
|
||||
//judge whether instance is intesected with plate or not
|
||||
@@ -6466,7 +6470,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
|
||||
plate_data_item->filament_change_sequence = m_plate_list[i]->m_gcode_result->filament_change_sequence;
|
||||
plate_data_item->nozzle_change_sequence = m_plate_list[i]->m_gcode_result->nozzle_change_sequence;
|
||||
plate_data_item->optimal_assignment = m_plate_list[i]->m_gcode_result->optimal_assignment;
|
||||
plate_data_item->first_layer_time = std::to_string(m_plate_list[i]->m_gcode_result->initial_layer_time);
|
||||
plate_data_item->first_layer_time = std::to_string(m_plate_list[i]->cali_bboxes_data.first_layer_time);
|
||||
Print *print = nullptr;
|
||||
m_plate_list[i]->get_print((PrintBase **) &print, nullptr, nullptr);
|
||||
if (print) {
|
||||
|
||||
@@ -681,7 +681,7 @@ SearchDialog::SearchDialog(OptionsSearcher *searcher, Preset::Type type, wxWindo
|
||||
|
||||
SearchDialog::~SearchDialog() {}
|
||||
|
||||
void SearchDialog::Popup(wxWindow *focus /*= nullptr*/)
|
||||
void SearchDialog::Popup(wxPoint position /*= wxDefaultPosition*/)
|
||||
{
|
||||
/* const std::string& line = searcher->search_string();
|
||||
search_line->SetValue(line.empty() ? default_string : from_u8(line));
|
||||
@@ -696,19 +696,17 @@ void SearchDialog::Popup(wxWindow *focus /*= nullptr*/)
|
||||
search_line2->SetValue(wxString(""));
|
||||
//const std::string &line = searcher->search_string();
|
||||
//searcher->search(into_u8(line), true);
|
||||
PopupWindow::Popup(focus);
|
||||
PopupWindow::Popup();
|
||||
search_line2->SetFocus();
|
||||
update_list();
|
||||
}
|
||||
|
||||
|
||||
#ifdef __WXMSW__
|
||||
void SearchDialog::MSWDismissUnfocusedPopup()
|
||||
{
|
||||
Dismiss();
|
||||
OnDismiss();
|
||||
}
|
||||
#endif // __WXMSW__
|
||||
|
||||
void SearchDialog::OnDismiss() { }
|
||||
|
||||
@@ -928,7 +926,7 @@ SearchObjectDialog::SearchObjectDialog(GUI::ObjectList* object_list, wxWindow* p
|
||||
|
||||
SearchObjectDialog::~SearchObjectDialog() {}
|
||||
|
||||
void SearchObjectDialog::Popup(wxWindow *focus /*= nullptr*/)
|
||||
void SearchObjectDialog::Popup(wxPoint position /*= wxDefaultPosition*/)
|
||||
{
|
||||
if (m_is_dismissing || this->IsShown()) {
|
||||
return;
|
||||
@@ -939,7 +937,7 @@ void SearchObjectDialog::Popup(wxWindow *focus /*= nullptr*/)
|
||||
// dropdown list, otherwise the text input won't be usable
|
||||
m_object_list->SetFocus();
|
||||
#endif
|
||||
PopupWindow::Popup(focus);
|
||||
PopupWindow::Popup();
|
||||
search_line2->SetFocus();
|
||||
|
||||
m_object_list->assembly_plate_object_name();
|
||||
@@ -947,13 +945,11 @@ void SearchObjectDialog::Popup(wxWindow *focus /*= nullptr*/)
|
||||
update_list();
|
||||
}
|
||||
|
||||
#ifdef __WXMSW__
|
||||
void SearchObjectDialog::MSWDismissUnfocusedPopup()
|
||||
{
|
||||
Dismiss();
|
||||
OnDismiss();
|
||||
}
|
||||
#endif // __WXMSW__
|
||||
|
||||
void SearchObjectDialog::OnDismiss() {}
|
||||
|
||||
|
||||
@@ -216,12 +216,10 @@ public:
|
||||
SearchDialog(OptionsSearcher *searcher, Preset::Type type, wxWindow *parent, TextInput *input, wxWindow *search_btn);
|
||||
~SearchDialog();
|
||||
|
||||
#ifdef __WXMSW__
|
||||
void MSWDismissUnfocusedPopup() override;
|
||||
#endif // __WXMSW__
|
||||
void Popup(wxWindow *focus = nullptr) override;
|
||||
void OnDismiss() override;
|
||||
void Dismiss() override;
|
||||
void MSWDismissUnfocusedPopup();
|
||||
void Popup(wxPoint position = wxDefaultPosition);
|
||||
void OnDismiss();
|
||||
void Dismiss();
|
||||
void Die();
|
||||
void msw_rescale();
|
||||
|
||||
@@ -262,12 +260,10 @@ public:
|
||||
SearchObjectDialog(GUI::ObjectList* object_list, wxWindow* parent, TextInput* input);
|
||||
~SearchObjectDialog();
|
||||
|
||||
#ifdef __WXMSW__
|
||||
void MSWDismissUnfocusedPopup() override;
|
||||
#endif // __WXMSW__
|
||||
void Popup(wxWindow *focus = nullptr) override;
|
||||
void OnDismiss() override;
|
||||
void Dismiss() override;
|
||||
void MSWDismissUnfocusedPopup();
|
||||
void Popup(wxPoint position = wxDefaultPosition);
|
||||
void OnDismiss();
|
||||
void Dismiss();
|
||||
void Die();
|
||||
|
||||
void OnInputText(wxCommandEvent& event);
|
||||
|
||||
@@ -98,7 +98,7 @@ void LabeledStaticBox::SetBorderColor(StateColor const &color)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
bool LabeledStaticBox::SetFont(const wxFont &set_font)
|
||||
void LabeledStaticBox::SetFont(wxFont set_font)
|
||||
{
|
||||
m_font = set_font;
|
||||
|
||||
@@ -109,7 +109,6 @@ bool LabeledStaticBox::SetFont(const wxFont &set_font)
|
||||
m_label_width = tW;
|
||||
|
||||
Refresh();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LabeledStaticBox::Enable(bool enable)
|
||||
|
||||
@@ -42,7 +42,7 @@ public:
|
||||
|
||||
void SetBorderColor(StateColor const &color);
|
||||
|
||||
bool SetFont(const wxFont &set_font) override;
|
||||
void SetFont(wxFont set_font);
|
||||
|
||||
bool Enable(bool enable) override;
|
||||
|
||||
|
||||
@@ -21,8 +21,6 @@ ScrolledWindow::ScrolledWindow(wxWindow *parent, wxWindowID id, wxPoint position
|
||||
m_bottomScrollbar = NULL;
|
||||
m_verticalSplitter = NULL;
|
||||
m_horizontalSplitter = NULL;
|
||||
m_userPanel = NULL;
|
||||
m_scroll_win = NULL;
|
||||
|
||||
m_marginWidth = marginWidth;
|
||||
|
||||
@@ -112,13 +110,12 @@ void ScrolledWindow::SetTipColor(wxColour color)
|
||||
if (m_bottomScrollbar) m_bottomScrollbar->SetTipColor(color);
|
||||
}
|
||||
|
||||
bool ScrolledWindow::SetBackgroundColour(const wxColour &color)
|
||||
void ScrolledWindow::SetBackgroundColour(wxColour color)
|
||||
{
|
||||
const bool result = wxWindow::SetBackgroundColour(color);
|
||||
wxWindow::SetBackgroundColour(color);
|
||||
m_verticalSplitter->SetBackgroundColour(color);
|
||||
m_userPanel->SetBackgroundColour(color);
|
||||
m_scroll_win->SetBackgroundColour(color);
|
||||
return result;
|
||||
}
|
||||
|
||||
void ScrolledWindow::SetMarginColor(wxColour color)
|
||||
|
||||
@@ -15,7 +15,7 @@ public:
|
||||
ScrolledWindow(wxWindow *parent, wxWindowID id, wxPoint position, wxSize size, long style, int marginWidth = 0, int scrollbarWidth = 4, int tipLength = 0);
|
||||
void OnMouseWheel(wxMouseEvent &event);
|
||||
void SetTipColor(wxColour color);
|
||||
bool SetBackgroundColour(const wxColour &color) override;
|
||||
void SetBackgroundColour(wxColour color);
|
||||
|
||||
void SetMarginColor(wxColour color);
|
||||
void SetScrollbarColor(wxColour color);
|
||||
@@ -26,7 +26,7 @@ public:
|
||||
// wxSplitterWindow* GetVerticalSplitter() { return m_verticalSplitter; }
|
||||
// wxSplitterWindow* GetHorizontalSplitter() { return m_horizontalSplitter; }
|
||||
bool IsBothDirections() { return m_bothDirections; }
|
||||
virtual void SetScrollbars(int pixelsPerUnitX, int pixelsPerUnitY, int noUnitsX, int noUnitsY, int xPos = 0, int yPos = 0, bool noRefresh = false) override;
|
||||
virtual void SetScrollbars(int pixelsPerUnitX, int pixelsPerUnitY, int noUnitsX, int noUnitsY, int xPos = 0, int yPos = 0, bool noRefresh = false);
|
||||
|
||||
private:
|
||||
wxPanel * m_userPanel; // the panel targeted by the scrolled window
|
||||
|
||||
@@ -7,11 +7,127 @@
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <future>
|
||||
#include <slic3r/GUI/BindDialog.hpp>
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/plugin/PluginFsUtils.hpp>
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <wx/event.h>
|
||||
#include <wx/msgdlg.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// extensive list of audit events can be found at https://docs.python.org/3/library/audit_events.html
|
||||
static const std::unordered_map<std::string, AuditEventCategory> audit_event_categories{
|
||||
// fsread
|
||||
{"glob.glob", AuditEventCategory::FsRead},
|
||||
{"glob.glob/2", AuditEventCategory::FsRead},
|
||||
{"os.fwalk", AuditEventCategory::FsRead},
|
||||
{"os.getxattr", AuditEventCategory::FsRead},
|
||||
{"os.listdir", AuditEventCategory::FsRead},
|
||||
{"os.listdrives", AuditEventCategory::FsRead},
|
||||
{"os.listmounts", AuditEventCategory::FsRead},
|
||||
{"os.listvolumes", AuditEventCategory::FsRead},
|
||||
{"os.listxattr", AuditEventCategory::FsRead},
|
||||
{"os.scandir", AuditEventCategory::FsRead},
|
||||
{"os.walk", AuditEventCategory::FsRead},
|
||||
{"pathlib.Path.glob", AuditEventCategory::FsRead},
|
||||
{"pathlib.Path.rglob", AuditEventCategory::FsRead},
|
||||
|
||||
// fsreadwrite
|
||||
{"os.chflags", AuditEventCategory::FsReadWrite},
|
||||
{"os.chmod", AuditEventCategory::FsReadWrite},
|
||||
{"os.chown", AuditEventCategory::FsReadWrite},
|
||||
{"os.removexattr", AuditEventCategory::FsReadWrite},
|
||||
{"os.rename", AuditEventCategory::FsReadWrite},
|
||||
{"os.setxattr", AuditEventCategory::FsReadWrite},
|
||||
{"os.truncate", AuditEventCategory::FsReadWrite},
|
||||
{"os.utime", AuditEventCategory::FsReadWrite},
|
||||
{"shutil.chown", AuditEventCategory::FsReadWrite},
|
||||
{"shutil.copymode", AuditEventCategory::FsReadWrite},
|
||||
{"shutil.copystat", AuditEventCategory::FsReadWrite},
|
||||
{"shutil.copyfile", AuditEventCategory::FsReadWrite},
|
||||
{"shutil.copytree", AuditEventCategory::FsReadWrite},
|
||||
{"shutil.make_archive", AuditEventCategory::FsReadWrite},
|
||||
{"shutil.move", AuditEventCategory::FsReadWrite},
|
||||
{"shutil.unpack_archive", AuditEventCategory::FsReadWrite},
|
||||
|
||||
// fscreate
|
||||
{"os.link", AuditEventCategory::FsCreate},
|
||||
{"os.mkdir", AuditEventCategory::FsCreate},
|
||||
{"os.symlink", AuditEventCategory::FsCreate},
|
||||
{"tempfile.mkdtemp", AuditEventCategory::FsCreate},
|
||||
{"tempfile.mkstemp", AuditEventCategory::FsCreate},
|
||||
{"_winapi.CreateJunction", AuditEventCategory::FsCreate},
|
||||
|
||||
// fsdelete
|
||||
{"os.remove", AuditEventCategory::FsDelete},
|
||||
{"os.rmdir", AuditEventCategory::FsDelete},
|
||||
{"shutil.rmtree", AuditEventCategory::FsDelete},
|
||||
|
||||
// http
|
||||
{"http.client.connect", AuditEventCategory::Http},
|
||||
{"http.client.send", AuditEventCategory::Http},
|
||||
{"urllib.Request", AuditEventCategory::Http},
|
||||
|
||||
// socket
|
||||
{"socket.__new__", AuditEventCategory::Socket},
|
||||
{"socket.bind", AuditEventCategory::Socket},
|
||||
{"socket.connect", AuditEventCategory::Socket},
|
||||
{"socket.getaddrinfo", AuditEventCategory::Socket},
|
||||
{"socket.gethostbyaddr", AuditEventCategory::Socket},
|
||||
{"socket.gethostbyname", AuditEventCategory::Socket},
|
||||
{"socket.gethostname", AuditEventCategory::Socket},
|
||||
{"socket.getnameinfo", AuditEventCategory::Socket},
|
||||
{"socket.getservbyname", AuditEventCategory::Socket},
|
||||
{"socket.getservbyport", AuditEventCategory::Socket},
|
||||
{"socket.sendmsg", AuditEventCategory::Socket},
|
||||
{"socket.sendto", AuditEventCategory::Socket},
|
||||
|
||||
// processcreate
|
||||
{"os.fork", AuditEventCategory::ProcessCreate},
|
||||
{"os.forkpty", AuditEventCategory::ProcessCreate},
|
||||
{"os.posix_spawn", AuditEventCategory::ProcessCreate},
|
||||
{"os.spawn", AuditEventCategory::ProcessCreate},
|
||||
{"os.system", AuditEventCategory::ProcessCreate},
|
||||
{"os.startfile", AuditEventCategory::ProcessCreate},
|
||||
{"os.startfile/2", AuditEventCategory::ProcessCreate},
|
||||
{"pty.spawn", AuditEventCategory::ProcessCreate},
|
||||
{"subprocess.Popen", AuditEventCategory::ProcessCreate},
|
||||
{"_winapi.CreateProcess", AuditEventCategory::ProcessCreate},
|
||||
{"_posixsubprocess.fork_exec", AuditEventCategory::ProcessCreate},
|
||||
};
|
||||
|
||||
// Returns the category event_name belongs to, or AuditEventCategory::None when it isn't audited.
|
||||
static AuditEventCategory event_category(const std::string& event_name)
|
||||
{
|
||||
const auto it = audit_event_categories.find(event_name);
|
||||
return it == audit_event_categories.end() ? AuditEventCategory::None : it->second;
|
||||
}
|
||||
|
||||
// True for the categories whose targets are filesystem paths, as opposed to a network
|
||||
// address or a process command line -- the deny-path and allowed-root checks only make sense
|
||||
// against a path.
|
||||
static bool is_fs_category(AuditEventCategory category)
|
||||
{
|
||||
switch (category) {
|
||||
case AuditEventCategory::FsRead:
|
||||
case AuditEventCategory::FsReadWrite:
|
||||
case AuditEventCategory::FsCreate:
|
||||
case AuditEventCategory::FsDelete:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Path safety
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -78,24 +194,21 @@ bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boos
|
||||
// ScopedPluginAuditContext
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
thread_local std::string PluginAuditManager::m_current_plugin_key = "";
|
||||
thread_local std::string PluginAuditManager::m_current_capability_name = "";
|
||||
thread_local PluginAuditManager::AuditMode PluginAuditManager::m_audit_mode = PluginAuditManager::AuditMode::Loading;
|
||||
thread_local std::vector<boost::filesystem::path> PluginAuditManager::m_scoped_allowed_roots;
|
||||
thread_local std::string PluginAuditManager::m_current_plugin_key = "";
|
||||
thread_local std::string PluginAuditManager::m_current_capability_name = "";
|
||||
thread_local std::vector<AllowedRoot> PluginAuditManager::m_scoped_allowed_roots;
|
||||
thread_local bool PluginAuditManager::m_audit_denial_pending = false;
|
||||
thread_local bool PluginAuditManager::m_has_last_violation = false;
|
||||
thread_local AuditViolation PluginAuditManager::m_last_violation;
|
||||
|
||||
ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key,
|
||||
const std::string& capability_name,
|
||||
PluginAuditManager::AuditMode mode)
|
||||
const std::string& capability_name)
|
||||
: m_previous_id(PluginAuditManager::instance().current_plugin())
|
||||
, m_previous_capability(PluginAuditManager::instance().current_capability())
|
||||
, m_previous_mode(PluginAuditManager::instance().audit_mode())
|
||||
, m_previous_scoped_roots(PluginAuditManager::m_scoped_allowed_roots)
|
||||
{
|
||||
PluginAuditManager::instance().set_current_plugin(plugin_key);
|
||||
PluginAuditManager::instance().set_current_capability(capability_name);
|
||||
PluginAuditManager::instance().set_audit_mode(mode);
|
||||
PluginAuditManager::m_scoped_allowed_roots.clear();
|
||||
}
|
||||
|
||||
@@ -103,7 +216,6 @@ ScopedPluginAuditContext::~ScopedPluginAuditContext()
|
||||
{
|
||||
PluginAuditManager::instance().set_current_plugin(m_previous_id);
|
||||
PluginAuditManager::instance().set_current_capability(m_previous_capability);
|
||||
PluginAuditManager::instance().set_audit_mode(m_previous_mode);
|
||||
PluginAuditManager::m_scoped_allowed_roots = std::move(m_previous_scoped_roots);
|
||||
}
|
||||
|
||||
@@ -129,23 +241,24 @@ std::string PluginAuditManager::current_capability() const { return m_current_ca
|
||||
|
||||
void PluginAuditManager::clear_current_capability() { m_current_capability_name.clear(); }
|
||||
|
||||
void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root)
|
||||
void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root, bool allow_write)
|
||||
{
|
||||
if (root.empty())
|
||||
return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_global_allowed_roots.push_back(root);
|
||||
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string();
|
||||
m_global_allowed_roots.push_back({root, allow_write});
|
||||
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string() << " allow_write=" << allow_write;
|
||||
}
|
||||
|
||||
void PluginAuditManager::add_scoped_allowed_root(const boost::filesystem::path& root)
|
||||
void PluginAuditManager::add_scoped_allowed_root(const boost::filesystem::path& root, bool allow_write)
|
||||
{
|
||||
if (root.empty())
|
||||
return;
|
||||
|
||||
m_scoped_allowed_roots.push_back(root);
|
||||
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Scoped allowed root for plugin " << current_plugin() << ": " << root.string();
|
||||
m_scoped_allowed_roots.push_back({root, allow_write});
|
||||
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Scoped allowed root for plugin " << current_plugin() << ": " << root.string()
|
||||
<< " allow_write=" << allow_write;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -199,12 +312,65 @@ bool PluginAuditManager::is_denied_filename(const boost::filesystem::path& candi
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Audit mode
|
||||
// Denied path keywords
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void PluginAuditManager::set_audit_mode(AuditMode mode) { m_audit_mode = mode; }
|
||||
void PluginAuditManager::add_denied_path_keyword(const std::string& keyword)
|
||||
{
|
||||
if (keyword.empty())
|
||||
return;
|
||||
|
||||
PluginAuditManager::AuditMode PluginAuditManager::audit_mode() const { return m_audit_mode; }
|
||||
std::string lower = keyword;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_denied_path_keywords.push_back(lower);
|
||||
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Denied path keyword: " << lower;
|
||||
}
|
||||
|
||||
std::vector<std::string> PluginAuditManager::default_denied_path_keywords()
|
||||
{
|
||||
// Broad, categorical rules on top of the exact-name is_denied_filename registry: a plugin
|
||||
// must never be able to reach a secret, a certificate, or a configuration file just because
|
||||
// it happens to live inside an otherwise-allowed root (e.g. the bundled TLS client cert at
|
||||
// resources_dir()/cert/..., which would become reachable the moment resources_dir() is
|
||||
// granted as a read-only allowed root).
|
||||
return {"secret", "cert", "conf"};
|
||||
}
|
||||
|
||||
bool PluginAuditManager::is_denied_path_keyword(const boost::filesystem::path& candidate) const
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
boost::system::error_code ec;
|
||||
fs::path canon = fs::weakly_canonical(candidate, ec);
|
||||
if (ec) {
|
||||
canon = fs::absolute(candidate, ec).lexically_normal();
|
||||
if (ec)
|
||||
canon = candidate;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_denied_path_keywords.empty())
|
||||
return false;
|
||||
|
||||
for (const auto& component : canon) {
|
||||
std::string name = component.string();
|
||||
if (name.empty())
|
||||
continue;
|
||||
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); });
|
||||
for (const auto& keyword : m_denied_path_keywords) {
|
||||
if (name.find(keyword) != std::string::npos)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PluginAuditManager::is_denied_path(const boost::filesystem::path& candidate) const
|
||||
{
|
||||
return is_denied_filename(candidate) || is_denied_path_keyword(candidate);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Policy checks
|
||||
@@ -219,20 +385,20 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
|
||||
if (plugin_key.empty())
|
||||
return {true, ""}; // not running inside a plugin context
|
||||
|
||||
// Denied filenames are checked first, above both the Loading exemption below and the
|
||||
// allowed roots. The app config and the cloud refresh token live directly inside
|
||||
// data_dir(), which is a global allowed root, and no scope ever sets Enforcing — so a
|
||||
// deny placed any lower would be unreachable for reads.
|
||||
// Denied filenames/keywords are checked before the allowed roots. The app config and the
|
||||
// cloud refresh token live directly inside data_dir(), which is a global allowed root, and
|
||||
// the bundled TLS client cert lives inside resources_dir(), a read-only global allowed
|
||||
// root, so a deny placed any lower would be unreachable.
|
||||
if (is_denied_filename(path)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write
|
||||
<< " plugin=" << plugin_key << " reason=denied filename";
|
||||
return {false, "denied filename"};
|
||||
}
|
||||
|
||||
// During import/loading, only block writes. Python must be able to read
|
||||
// stdlib modules and the plugin file itself during import.
|
||||
if (m_audit_mode == AuditMode::Loading && !is_write)
|
||||
return {true, ""};
|
||||
if (is_denied_path_keyword(path)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write
|
||||
<< " plugin=" << plugin_key << " reason=denied path keyword";
|
||||
return {false, "denied path keyword"};
|
||||
}
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
fs::path candidate = path;
|
||||
@@ -245,8 +411,11 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
|
||||
candidate = absolute_candidate;
|
||||
}
|
||||
|
||||
// A root that doesn't allow writes only matches a read-shaped request; a write/create/
|
||||
// delete-shaped one falls through to "outside allowed root" for that root even though the
|
||||
// path is physically inside it.
|
||||
for (const auto& root : m_scoped_allowed_roots) {
|
||||
if (is_inside_allowed_root(candidate, root)) {
|
||||
if ((!is_write || root.allow_write) && is_inside_allowed_root(candidate, root.path)) {
|
||||
return {true, ""};
|
||||
}
|
||||
}
|
||||
@@ -254,14 +423,13 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (const auto& root : m_global_allowed_roots) {
|
||||
if (is_inside_allowed_root(candidate, root)) {
|
||||
if ((!is_write || root.allow_write) && is_inside_allowed_root(candidate, root.path)) {
|
||||
return {true, ""};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << candidate.string() << " is_write=" << is_write
|
||||
<< " audit_mode=" << (m_audit_mode == AuditMode::Loading ? "Loading" : "Enforcing")
|
||||
<< " plugin=" << plugin_key;
|
||||
return {false, "outside allowed root"};
|
||||
}
|
||||
@@ -269,19 +437,99 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
|
||||
AuditDecision PluginAuditManager::check_open(const std::string& path_str, const std::string& mode)
|
||||
{
|
||||
const bool is_write = mode.find('w') != std::string::npos || mode.find('a') != std::string::npos ||
|
||||
mode.find('+') != std::string::npos;
|
||||
mode.find('+') != std::string::npos || mode.find('x') != std::string::npos;
|
||||
return check_path_access(boost::filesystem::path(path_str), is_write);
|
||||
}
|
||||
|
||||
bool PluginAuditManager::request_filesystem_read_permissions(const std::string& plugin_key,
|
||||
const std::vector<std::string>& paths)
|
||||
{
|
||||
if (plugin_key.empty() || paths.empty())
|
||||
return true;
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
|
||||
return false;
|
||||
|
||||
PluginInstallState state;
|
||||
read_install_state(boost::filesystem::path(descriptor.plugin_root), state);
|
||||
|
||||
std::vector<std::string> missing;
|
||||
for (const std::string& path : paths) {
|
||||
if (std::find(state.permissions.fs_read.begin(), state.permissions.fs_read.end(), path) == state.permissions.fs_read.end())
|
||||
missing.push_back(path);
|
||||
}
|
||||
if (missing.empty())
|
||||
return true;
|
||||
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
|
||||
return false;
|
||||
|
||||
auto show_dialog = [&descriptor, &missing]() {
|
||||
wxString requested_paths;
|
||||
for (const std::string& path : missing)
|
||||
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
|
||||
|
||||
wxMessageDialog dialog(
|
||||
nullptr,
|
||||
wxString::Format("Plugin \"%s\" requests filesystem read access to:\n%s",
|
||||
wxString::FromUTF8(descriptor.name.c_str()), requested_paths),
|
||||
"Plugin permissions",
|
||||
wxYES_NO | wxICON_WARNING);
|
||||
return dialog.ShowModal() == wxID_YES;
|
||||
};
|
||||
|
||||
bool granted = false;
|
||||
if (wxIsMainThread()) {
|
||||
granted = show_dialog();
|
||||
} else {
|
||||
auto result = std::make_shared<std::promise<bool>>();
|
||||
auto future = result->get_future();
|
||||
GUI::wxGetApp().CallAfter([result, descriptor_name = descriptor.name, missing]() {
|
||||
wxString requested_paths;
|
||||
for (const std::string& path : missing)
|
||||
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
|
||||
|
||||
wxMessageDialog dialog(
|
||||
nullptr,
|
||||
wxString::Format("Plugin \"%s\" requests filesystem read access to:\n%s",
|
||||
wxString::FromUTF8(descriptor_name.c_str()), requested_paths),
|
||||
"Plugin permissions",
|
||||
wxYES_NO | wxICON_WARNING);
|
||||
result->set_value(dialog.ShowModal() == wxID_YES);
|
||||
});
|
||||
granted = future.get();
|
||||
}
|
||||
|
||||
if (!granted)
|
||||
return false;
|
||||
|
||||
if (state.plugin_name.empty()) {
|
||||
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
|
||||
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
|
||||
state.plugin_name = descriptor.name;
|
||||
state.cloud_uuid = descriptor.cloud_uuid();
|
||||
state.enabled = true;
|
||||
}
|
||||
|
||||
state.permissions.fs_read.insert(state.permissions.fs_read.end(), missing.begin(), missing.end());
|
||||
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
|
||||
}
|
||||
|
||||
void PluginAuditManager::report_violation(const AuditViolation& violation)
|
||||
{
|
||||
m_last_violation = violation;
|
||||
m_has_last_violation = true;
|
||||
m_audit_denial_pending = true;
|
||||
|
||||
BOOST_LOG_TRIVIAL(warning) << "[AUDIT BLOCKED] plugin=" << violation.plugin_key << " event=" << violation.event_name
|
||||
<< " path=" << violation.path.string() << " reason=" << violation.reason;
|
||||
<< " reason=" << violation.reason;
|
||||
}
|
||||
|
||||
bool PluginAuditManager::audit_denial_pending() const { return m_audit_denial_pending; }
|
||||
|
||||
void PluginAuditManager::clear_audit_denial() { m_audit_denial_pending = false; }
|
||||
|
||||
void PluginAuditManager::clear_last_violation()
|
||||
{
|
||||
m_has_last_violation = false;
|
||||
@@ -297,31 +545,326 @@ bool PluginAuditManager::last_violation(AuditViolation& violation) const
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginAuditManager::has_approved_ancestor(const std::string& plugin_key,
|
||||
const std::vector<std::string>& call_site_ids) const
|
||||
{
|
||||
if (plugin_key.empty() || call_site_ids.empty())
|
||||
return false;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
auto it = m_approved_call_sites.find(plugin_key);
|
||||
if (it == m_approved_call_sites.end())
|
||||
return false;
|
||||
|
||||
for (const auto& id : call_site_ids)
|
||||
if (it->second.count(id))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void PluginAuditManager::record_approved_call_sites(const std::string& plugin_key,
|
||||
const std::vector<std::string>& call_site_ids)
|
||||
{
|
||||
if (plugin_key.empty() || call_site_ids.empty())
|
||||
return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
auto& approved = m_approved_call_sites[plugin_key];
|
||||
approved.insert(call_site_ids.begin(), call_site_ids.end());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The C-level audit hook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
namespace PluginAuditDetail {
|
||||
|
||||
PyObject* tuple_item(PyObject* args, Py_ssize_t index)
|
||||
{
|
||||
if (!args || !PyTuple_Check(args) || index < 0 || index >= PyTuple_GET_SIZE(args))
|
||||
return nullptr;
|
||||
return PyTuple_GET_ITEM(args, index);
|
||||
}
|
||||
|
||||
std::string python_path(PyObject* object)
|
||||
{
|
||||
if (!object)
|
||||
return {};
|
||||
|
||||
PyObject* path_object = PyOS_FSPath(object);
|
||||
if (!path_object) {
|
||||
PyErr_Clear();
|
||||
return {};
|
||||
}
|
||||
|
||||
const char* path = PyUnicode_Check(path_object) ? PyUnicode_AsUTF8(path_object) : PyBytes_AsString(path_object);
|
||||
std::string result = path ? path : "";
|
||||
Py_DECREF(path_object);
|
||||
if (!path)
|
||||
PyErr_Clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string python_unicode(PyObject* object)
|
||||
{
|
||||
if (!object || !PyUnicode_Check(object))
|
||||
return {};
|
||||
const char* text = PyUnicode_AsUTF8(object);
|
||||
if (!text) {
|
||||
PyErr_Clear();
|
||||
return {};
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string python_str(PyObject* object)
|
||||
{
|
||||
if (!object)
|
||||
return {};
|
||||
|
||||
PyObject* str_object = PyObject_Str(object);
|
||||
if (!str_object) {
|
||||
PyErr_Clear();
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::string result = python_unicode(str_object);
|
||||
Py_DECREF(str_object);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> call_site_identities(const std::string& plugin_root)
|
||||
{
|
||||
std::vector<std::string> ids;
|
||||
if (plugin_root.empty())
|
||||
return ids;
|
||||
|
||||
PyFrameObject* frame = PyEval_GetFrame(); // borrowed reference
|
||||
Py_XINCREF(frame); // normalize to an owned reference for the loop below
|
||||
|
||||
while (frame) {
|
||||
PyCodeObject* code = PyFrame_GetCode(frame); // new reference
|
||||
const std::string filename = python_unicode(reinterpret_cast<PyObject*>(code->co_filename));
|
||||
const bool is_plugin_frame = !filename.empty() && boost::algorithm::starts_with(filename, plugin_root);
|
||||
|
||||
std::string id;
|
||||
if (!is_plugin_frame && !filename.empty()) {
|
||||
const std::string funcname = python_unicode(reinterpret_cast<PyObject*>(code->co_name));
|
||||
id = filename + ":" + funcname + ":" + std::to_string(code->co_firstlineno);
|
||||
}
|
||||
Py_DECREF(code);
|
||||
|
||||
PyFrameObject* back = PyFrame_GetBack(frame); // new reference, or nullptr at the top of the stack
|
||||
Py_DECREF(frame);
|
||||
frame = back;
|
||||
|
||||
if (is_plugin_frame)
|
||||
break;
|
||||
if (!id.empty())
|
||||
ids.push_back(std::move(id));
|
||||
}
|
||||
|
||||
Py_XDECREF(frame); // only holds a reference here if the loop exited via break
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
static const std::unordered_set<std::string> two_path_fs_events{
|
||||
"os.rename", "os.link", "os.symlink", "shutil.copyfile",
|
||||
"shutil.copytree", "shutil.copymode", "shutil.copystat", "shutil.move",
|
||||
"shutil.unpack_archive", "_winapi.CreateJunction",
|
||||
};
|
||||
|
||||
static const std::unordered_map<std::string, std::vector<Py_ssize_t>> audit_target_arg_indices{
|
||||
{"http.client.connect", {1}},
|
||||
{"urllib.Request", {0}},
|
||||
|
||||
{"socket.connect", {1}},
|
||||
{"socket.bind", {1}},
|
||||
{"socket.getaddrinfo", {0}},
|
||||
{"socket.gethostbyname", {0}},
|
||||
{"socket.gethostbyaddr", {0}},
|
||||
{"socket.getnameinfo", {0}},
|
||||
{"socket.getservbyname", {0}},
|
||||
{"socket.getservbyport", {0}},
|
||||
|
||||
{"os.system", {0}},
|
||||
{"subprocess.Popen", {1, 0}},
|
||||
{"os.posix_spawn", {1, 0}},
|
||||
{"os.spawn", {2, 1}},
|
||||
{"os.startfile", {0}},
|
||||
{"pty.spawn", {0}},
|
||||
{"_winapi.CreateProcess", {1, 0}},
|
||||
{"_posixsubprocess.fork_exec", {0}},
|
||||
};
|
||||
|
||||
AuditEventCategory open_category(PyObject* args)
|
||||
{
|
||||
const std::string mode = python_unicode(tuple_item(args, 1));
|
||||
if (!mode.empty() && mode.find_first_of("wax+") == std::string::npos)
|
||||
return AuditEventCategory::FsRead;
|
||||
return AuditEventCategory::FsReadWrite;
|
||||
}
|
||||
|
||||
std::vector<std::string> audit_targets(const std::string& event_name, AuditEventCategory category, PyObject* args)
|
||||
{
|
||||
std::vector<std::string> targets;
|
||||
|
||||
switch (category) {
|
||||
case AuditEventCategory::FsRead:
|
||||
case AuditEventCategory::FsReadWrite:
|
||||
case AuditEventCategory::FsCreate:
|
||||
case AuditEventCategory::FsDelete: {
|
||||
const Py_ssize_t path_count = two_path_fs_events.count(event_name) ? 2 : 1;
|
||||
for (Py_ssize_t index = 0; index < path_count; ++index) {
|
||||
const std::string path = python_path(tuple_item(args, index));
|
||||
if (!path.empty())
|
||||
targets.push_back(path);
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const auto it = audit_target_arg_indices.find(event_name);
|
||||
if (it != audit_target_arg_indices.end()) {
|
||||
for (const Py_ssize_t index : it->second) {
|
||||
std::string value = python_str(tuple_item(args, index));
|
||||
if (!value.empty()) {
|
||||
targets.push_back(std::move(value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
std::vector<std::string>* permission_list_for(AuditEventCategory category, PluginPermissions& permissions)
|
||||
{
|
||||
switch (category) {
|
||||
case AuditEventCategory::FsRead: return &permissions.fs_read;
|
||||
case AuditEventCategory::FsReadWrite: return &permissions.fs_readwrite;
|
||||
case AuditEventCategory::Http: return &permissions.network_http;
|
||||
case AuditEventCategory::Socket: return &permissions.network_socket;
|
||||
case AuditEventCategory::ProcessCreate: return &permissions.process;
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool has_permission(const std::vector<std::string>& granted, const std::string& target)
|
||||
{
|
||||
return std::find(granted.begin(), granted.end(), target) != granted.end();
|
||||
}
|
||||
|
||||
bool persist_permission(const std::string& plugin_key,
|
||||
PluginInstallState& state,
|
||||
std::vector<std::string>& permission_list,
|
||||
const std::string& target)
|
||||
{
|
||||
PluginDescriptor descriptor;
|
||||
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
|
||||
return false;
|
||||
|
||||
// A sandbox plugin may not have a sidecar yet. Seed the small amount of install metadata
|
||||
// needed by the writer so clicking Yes still creates the JSON file.
|
||||
if (state.plugin_name.empty()) {
|
||||
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
|
||||
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
|
||||
state.plugin_name = descriptor.name;
|
||||
state.cloud_uuid = descriptor.cloud_uuid();
|
||||
state.enabled = true;
|
||||
}
|
||||
|
||||
if (!has_permission(permission_list, target))
|
||||
permission_list.push_back(target);
|
||||
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
|
||||
}
|
||||
|
||||
// Records a blocked event and raises PermissionError in the calling interpreter. Returns -1
|
||||
// so an event branch can `return report_denied(...)` directly.
|
||||
int report_denied(PluginAuditManager& mgr,
|
||||
const std::string& event_name,
|
||||
const boost::filesystem::path& path,
|
||||
const AuditDecision& decision)
|
||||
{
|
||||
AuditViolation violation;
|
||||
violation.plugin_key = mgr.current_plugin();
|
||||
violation.event_name = event_name;
|
||||
violation.path = path;
|
||||
violation.reason = decision.reason;
|
||||
mgr.report_violation(violation);
|
||||
|
||||
PyErr_SetString(PyExc_PermissionError, "Plugin attempted to access a blocked file path");
|
||||
PyErr_SetString(PyExc_PermissionError, "Plugin attempted an audited operation without permission");
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
wxString audit_message(AuditEventCategory category, const wxString& plugin_name, const wxString& event_name,
|
||||
const wxString& target_list)
|
||||
{
|
||||
if (target_list.IsEmpty())
|
||||
return wxString::Format(
|
||||
_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\".\n\n"
|
||||
"This operation does not expose a target the audit hook can display."),
|
||||
plugin_name, event_name);
|
||||
|
||||
switch (category) {
|
||||
case AuditEventCategory::FsRead:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting to read the following file(s):\n%s"), plugin_name, target_list);
|
||||
case AuditEventCategory::FsReadWrite:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting to read/write the following file(s):\n%s"), plugin_name, target_list);
|
||||
case AuditEventCategory::FsCreate:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting to create the following file(s):\n%s"), plugin_name, target_list);
|
||||
case AuditEventCategory::FsDelete:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting to delete the following file(s):\n%s"), plugin_name, target_list);
|
||||
case AuditEventCategory::Http:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting to make an HTTP request to:\n%s"), plugin_name, target_list);
|
||||
case AuditEventCategory::Socket:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting to open a network connection to:\n%s"), plugin_name, target_list);
|
||||
case AuditEventCategory::ProcessCreate:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting to run the following command(s):\n%s"), plugin_name, target_list);
|
||||
default:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\"."), plugin_name, event_name);
|
||||
}
|
||||
}
|
||||
|
||||
int decide_audited_event(PluginAuditManager& mgr,
|
||||
PluginInstallState& state,
|
||||
const std::string& plugin_key,
|
||||
const std::string& plugin_name,
|
||||
const std::string& event_name,
|
||||
AuditEventCategory category,
|
||||
const std::vector<std::string>& targets,
|
||||
std::vector<std::string>* permission_list,
|
||||
const std::vector<std::string>& call_site_ids)
|
||||
{
|
||||
std::vector<std::string> unresolved = targets;
|
||||
if (permission_list) {
|
||||
unresolved.clear();
|
||||
for (const auto& target : targets)
|
||||
if (!has_permission(*permission_list, target))
|
||||
unresolved.push_back(target);
|
||||
if (!targets.empty() && unresolved.empty())
|
||||
return 0;
|
||||
}
|
||||
|
||||
wxString target_list;
|
||||
for (const auto& target : unresolved)
|
||||
target_list += wxString::FromUTF8(target.c_str()) + "\n";
|
||||
|
||||
wxMessageDialog dialog(nullptr,
|
||||
audit_message(category, wxString::FromUTF8(plugin_name.c_str()),
|
||||
wxString::FromUTF8(event_name.c_str()), target_list),
|
||||
_L("Plugin permission request"), wxYES_NO | wxICON_WARNING);
|
||||
if (dialog.ShowModal() != wxID_YES)
|
||||
return report_denied(mgr, event_name, {false, "audit permission required"});
|
||||
|
||||
if (permission_list)
|
||||
for (const auto& target : unresolved)
|
||||
persist_permission(plugin_key, state, *permission_list, target);
|
||||
|
||||
mgr.record_approved_call_sites(plugin_key, call_site_ids);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace PluginAuditDetail
|
||||
|
||||
int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data)
|
||||
{
|
||||
@@ -331,94 +874,95 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user
|
||||
|
||||
std::string event_name(event ? event : "");
|
||||
|
||||
if (event_name.empty())
|
||||
return 0;
|
||||
|
||||
// Verbose logging of every audit event (can be noisy)
|
||||
if (mgr->verbose_events) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "[AUDIT EVENT] " << event_name;
|
||||
}
|
||||
|
||||
// extensive list of audit events can be found at https://docs.python.org/3/library/audit_events.html
|
||||
|
||||
// --- open event ---
|
||||
if (event_name == "open") {
|
||||
const char* path_cstr = nullptr;
|
||||
const char* mode_cstr = nullptr;
|
||||
int flags = 0;
|
||||
|
||||
// open(path, mode, flags) — path may be str, bytes, or int fd
|
||||
if (!PyArg_ParseTuple(args, "s|si", &path_cstr, &mode_cstr, &flags)) {
|
||||
PyErr_Clear(); // couldn't parse; allow
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string path_str(path_cstr ? path_cstr : "");
|
||||
std::string mode_str(mode_cstr ? mode_cstr : "r");
|
||||
|
||||
AuditDecision decision = mgr->check_open(path_str, mode_str);
|
||||
if (!decision.allowed)
|
||||
return report_denied(*mgr, event_name, path_str, decision);
|
||||
if (mgr->current_plugin().empty()) {
|
||||
BOOST_LOG_TRIVIAL(trace) << "[AUDIT] event=" << event_name << " bypassed (no plugin context)";
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- os.rename event (raised by os.rename and os.replace) ---
|
||||
if (event_name == "os.rename") {
|
||||
const char* src_cstr = nullptr;
|
||||
const char* dst_cstr = nullptr;
|
||||
PyObject* src_dir_fd = nullptr;
|
||||
PyObject* dst_dir_fd = nullptr;
|
||||
|
||||
// os.rename(src, dst, src_dir_fd, dst_dir_fd) — paths may be str, bytes, or int fd.
|
||||
// The dir_fd arguments are unused, but must be accepted for the tuple to parse.
|
||||
if (!PyArg_ParseTuple(args, "ss|OO", &src_cstr, &dst_cstr, &src_dir_fd, &dst_dir_fd)) {
|
||||
PyErr_Clear(); // couldn't parse; allow
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A rename writes at both ends, so either end being denied blocks the call.
|
||||
for (const char* path_cstr : {src_cstr, dst_cstr}) {
|
||||
std::string path_str(path_cstr ? path_cstr : "");
|
||||
AuditDecision decision = mgr->check_path_access(path_str, /* is_write */ true);
|
||||
if (!decision.allowed)
|
||||
return report_denied(*mgr, event_name, path_str, decision);
|
||||
}
|
||||
// the open function can take in different flags that determine if it is a read or readwrite.
|
||||
const AuditEventCategory event_type =
|
||||
event_name == "open" ? PluginAuditDetail::open_category(args) : event_category(event_name);
|
||||
if (event_type == AuditEventCategory::None)
|
||||
return 0;
|
||||
|
||||
const bool fs_category = is_fs_category(event_type);
|
||||
const std::vector<std::string> targets = PluginAuditDetail::audit_targets(event_name, event_type, args);
|
||||
|
||||
// A denied path (secrets, certificates, config files -- see is_denied_path) is an
|
||||
// unconditional block: checked before the ancestor-cascade check below, so a cascade
|
||||
// approval recorded for an unrelated action can never launder access to one, and before
|
||||
// the allowed-root shortcut further down, so an allowed root (e.g. the read-only resources
|
||||
// folder) cannot make a denied path underneath it reachable.
|
||||
if (fs_category) {
|
||||
for (const auto& target : targets) {
|
||||
if (mgr->is_denied_path(boost::filesystem::path(target)))
|
||||
return PluginAuditDetail::report_denied(*mgr, event_name, {false, "denied path"});
|
||||
}
|
||||
}
|
||||
|
||||
// --- os.remove event (raised by os.remove and os.unlink) ---
|
||||
if (event_name == "os.remove") {
|
||||
const char* path_cstr = nullptr;
|
||||
PyObject* dir_fd = nullptr;
|
||||
PluginDescriptor plugin_descriptor;
|
||||
PluginManager::instance().try_get_plugin_descriptor(mgr->current_plugin(), plugin_descriptor);
|
||||
|
||||
// os.remove(path, dir_fd) — path may be str, bytes, or int fd
|
||||
if (!PyArg_ParseTuple(args, "s|O", &path_cstr, &dir_fd)) {
|
||||
PyErr_Clear(); // couldn't parse; allow
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string path_str(path_cstr ? path_cstr : "");
|
||||
AuditDecision decision = mgr->check_path_access(path_str, /* is_write */ true);
|
||||
if (!decision.allowed)
|
||||
return report_denied(*mgr, event_name, path_str, decision);
|
||||
// Some plugins call other audited events, e.g. urlib.request will call socket.connect. So this is so that if urlib.request
|
||||
// was already approved, socket.connect won't trigger another permission dialog.
|
||||
const std::vector<std::string> call_site_ids =
|
||||
PluginAuditDetail::call_site_identities(plugin_descriptor.plugin_root);
|
||||
if (mgr->has_approved_ancestor(mgr->current_plugin(), call_site_ids))
|
||||
return 0;
|
||||
|
||||
// A filesystem target that resolves entirely inside a pre-determined allowed root -- the
|
||||
// plugin system's own data_dir() tree (which holds each plugin's storage folder and the
|
||||
// installed/system profile cache), the read-only bundled resources folder, or a per-call
|
||||
// scoped root such as the current G-code folder -- is part of the plugin system's normal
|
||||
// workflow and does not need a prompt.
|
||||
if (fs_category && !targets.empty()) {
|
||||
const bool is_write = event_type != AuditEventCategory::FsRead;
|
||||
const bool all_inside_allowed_root =
|
||||
std::all_of(targets.begin(), targets.end(), [&](const std::string& target) {
|
||||
return mgr->check_path_access(boost::filesystem::path(target), is_write).allowed;
|
||||
});
|
||||
if (all_inside_allowed_root)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Unknown event — allow by default
|
||||
return 0;
|
||||
PluginInstallState state;
|
||||
const bool have_install_state = PluginManager::instance().get_install_state(mgr->current_plugin(), state);
|
||||
if (!have_install_state) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Failed to get install state for " << mgr->current_plugin();
|
||||
}
|
||||
|
||||
const std::string plugin_name = state.plugin_name.empty() ? mgr->current_plugin() : state.plugin_name;
|
||||
|
||||
std::vector<std::string>* permission_list = PluginAuditDetail::permission_list_for(event_type, state.permissions);
|
||||
|
||||
return PluginAuditDetail::decide_audited_event(*mgr, state, mgr->current_plugin(), plugin_name, event_name,
|
||||
event_type, targets, permission_list, call_site_ids);
|
||||
}
|
||||
|
||||
void PluginAuditManager::install_hook()
|
||||
{
|
||||
if (PySys_AddAuditHook(audit_hook, this) < 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook";
|
||||
return;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully";
|
||||
|
||||
// data_dir() is the only globally-allowed root during enforced plugin execution.
|
||||
// The executable directory and resources directory are intentionally NOT allowed
|
||||
// here: plugins must not write outside data_dir() (G-code plugins additionally get
|
||||
// the temp G-code folder via a scoped root). Reads remain permissive in Loading mode.
|
||||
// data_dir() is the primary globally-allowed root during plugin execution: read+write. It
|
||||
// covers the plugin system's own workflow needs -- each plugin's storage folder
|
||||
// (data_dir()/orca_plugins) and the installed/system profile cache (data_dir()/system) --
|
||||
// without a separate, narrower grant for either (G-code plugins additionally get the temp
|
||||
// G-code folder via a scoped root, see SlicingPipelinePluginCapabilityTrampoline).
|
||||
add_global_allowed_root(data_dir());
|
||||
|
||||
// resources_dir() holds the app's bundled, shared assets (installed system profiles, the
|
||||
// bundled TLS client cert, web assets). Plugins may read from it -- e.g. inspecting bundled
|
||||
// profiles -- but must never write into the shared, potentially multi-user app install, so
|
||||
// it is granted read-only. The bundled cert itself stays unreachable regardless, via the
|
||||
// "cert" denied-path keyword seeded below.
|
||||
add_global_allowed_root(resources_dir(), /*allow_write=*/false);
|
||||
|
||||
// The user's app config and cloud credentials live directly inside data_dir(), so the
|
||||
// root just granted would otherwise expose them to any plugin. Deny them by name.
|
||||
//
|
||||
@@ -430,6 +974,19 @@ void PluginAuditManager::install_hook()
|
||||
// (see its comment for why all four config names are denied); the tests seed from it too.
|
||||
for (const auto& name : default_denied_filenames())
|
||||
add_denied_filename(name);
|
||||
|
||||
// Categorical denies on top of the exact-name list above: no path a plugin can reach may
|
||||
// contain a "secret", "cert"(ificate), or "conf"(ig) path component, regardless of which
|
||||
// allowed root it happens to sit inside. default_denied_path_keywords() is the single
|
||||
// source of this list; the tests seed from it too.
|
||||
for (const auto& keyword : default_denied_path_keywords())
|
||||
add_denied_path_keyword(keyword);
|
||||
|
||||
if (PySys_AddAuditHook(audit_hook, this) < 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook";
|
||||
return;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully";
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
#define slic3r_PluginAuditManager_hpp_
|
||||
|
||||
#include <Python.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
@@ -18,10 +21,31 @@ struct AuditDecision {
|
||||
struct AuditViolation {
|
||||
std::string plugin_key;
|
||||
std::string event_name;
|
||||
boost::filesystem::path path;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
// A filesystem root a plugin may access while an audit context is active. allow_write is
|
||||
// false for a root that only grants reads (e.g. the bundled, shared resources folder) --
|
||||
// a write-shaped event never matches such a root, even though a read-shaped one does.
|
||||
struct AllowedRoot {
|
||||
boost::filesystem::path path;
|
||||
bool allow_write = true;
|
||||
};
|
||||
|
||||
// The set of CPython audit events PluginAuditManager recognizes, grouped by the kind of
|
||||
// operation they represent. None means the event isn't one audit_hook() acts on at all.
|
||||
enum class AuditEventCategory {
|
||||
None,
|
||||
FsRead,
|
||||
FsReadWrite,
|
||||
FsCreate,
|
||||
FsDelete,
|
||||
Http,
|
||||
Socket,
|
||||
ProcessCreate,
|
||||
Threading,
|
||||
};
|
||||
|
||||
// Returns true if candidate resolves to a path inside allowed_root.
|
||||
// Uses weakly_canonical and component-wise comparison to reject traversal attacks.
|
||||
bool is_inside_allowed_root(const boost::filesystem::path& candidate,
|
||||
@@ -49,12 +73,15 @@ public:
|
||||
void clear_current_capability();
|
||||
|
||||
// --- allowed-roots registry ---
|
||||
void add_global_allowed_root(const boost::filesystem::path& root);
|
||||
void add_scoped_allowed_root(const boost::filesystem::path& root);
|
||||
// allow_write = false registers a read-only root: a read-shaped event inside it is allowed,
|
||||
// but a write/create/delete-shaped event is not, so it falls through to the normal
|
||||
// prompt-or-deny path instead.
|
||||
void add_global_allowed_root(const boost::filesystem::path& root, bool allow_write = true);
|
||||
void add_scoped_allowed_root(const boost::filesystem::path& root, bool allow_write = true);
|
||||
|
||||
// --- denied-filenames registry ---
|
||||
// Filenames a plugin may never touch, in any directory, regardless of audit mode or
|
||||
// enclosing allowed root. A candidate is denied when its filename starts with a
|
||||
// Filenames a plugin may never touch, in any directory, regardless of the enclosing allowed
|
||||
// root. A candidate is denied when its filename starts with a
|
||||
// registered name, so .bak/.tmp companions are covered by the same entry.
|
||||
//
|
||||
// The comparison is case-insensitive on every platform, unlike the _WIN32-only iequals
|
||||
@@ -73,33 +100,60 @@ public:
|
||||
// 8.3 short name is out of scope (see the design doc). This blocks direct access only.
|
||||
bool is_denied_filename(const boost::filesystem::path& candidate) const;
|
||||
|
||||
// --- enforcement mode ---
|
||||
enum class AuditMode {
|
||||
// Import/loading phase: allow reads anywhere, only block writes
|
||||
// outside allowed roots. Python needs to read stdlib modules
|
||||
// during import and those are not inside plugin directories.
|
||||
Loading,
|
||||
// --- denied-path-keyword registry ---
|
||||
// Keywords that categorically deny a path if ANY of its components (directory or file
|
||||
// name), not just the base name, contains one case-insensitively -- e.g. a "secrets"
|
||||
// subfolder, a "certificates" folder, or a "conf"/"config" file anywhere the plugin can
|
||||
// otherwise reach, including inside an allowed root. This is intentionally broader and
|
||||
// fuzzier than the exact-name is_denied_filename registry: it exists to categorically rule
|
||||
// out whole classes of sensitive paths (secrets, certificates, config) rather than name
|
||||
// specific known files, at the cost of over-blocking an unrelated name that happens to
|
||||
// contain the keyword -- the fail-safe direction, same rationale as is_denied_filename.
|
||||
void add_denied_path_keyword(const std::string& keyword);
|
||||
|
||||
// Execution phase: block both reads and writes outside allowed
|
||||
// roots, plus subprocess/socket/ctypes.
|
||||
Enforcing,
|
||||
};
|
||||
// The list install_hook() seeds into the keyword registry. Exposed so tests seed the exact
|
||||
// same set without a live interpreter.
|
||||
static std::vector<std::string> default_denied_path_keywords();
|
||||
|
||||
void set_audit_mode(AuditMode mode);
|
||||
AuditMode audit_mode() const;
|
||||
// True when any component of candidate's (canonicalized) path contains a registered
|
||||
// keyword, case-insensitively.
|
||||
bool is_denied_path_keyword(const boost::filesystem::path& candidate) const;
|
||||
|
||||
// is_denied_filename(candidate) || is_denied_path_keyword(candidate). Convenience for
|
||||
// call sites that only need to know whether a path is categorically off-limits, not which
|
||||
// specific rule fired.
|
||||
bool is_denied_path(const boost::filesystem::path& candidate) const;
|
||||
|
||||
// --- policy checks ---
|
||||
// Shared core for every audited filesystem event. The deny list is consulted above the
|
||||
// Loading-mode read exemption and above the allowed roots, so a denied filename is
|
||||
// blocked even though every scope currently runs in Loading and the files in question
|
||||
// sit inside data_dir(), which is itself a global allowed root.
|
||||
// Shared core for every audited filesystem event. The deny checks are consulted above the
|
||||
// allowed roots, so a denied path is blocked even when it sits inside an allowed root (e.g.
|
||||
// data_dir(), which is a global allowed root).
|
||||
AuditDecision check_path_access(const boost::filesystem::path& candidate, bool is_write);
|
||||
AuditDecision check_open(const std::string& path, const std::string& mode);
|
||||
|
||||
// Ask the user to grant the requested filesystem-read paths. The request may originate on a
|
||||
// plugin load worker, so the implementation marshals the modal dialog to the wx main thread.
|
||||
// Returns true only when every missing path was granted and persisted; denial aborts the plugin
|
||||
// load without adding a permission.
|
||||
bool request_filesystem_read_permissions(const std::string& plugin_key,
|
||||
const std::vector<std::string>& paths);
|
||||
|
||||
void report_violation(const AuditViolation& violation);
|
||||
bool audit_denial_pending() const;
|
||||
void clear_audit_denial();
|
||||
void clear_last_violation();
|
||||
bool last_violation(AuditViolation& violation) const;
|
||||
|
||||
// --- call-site cascade cache ---
|
||||
// A single plugin action often fires several nested CPython audit events as it passes
|
||||
// through stdlib layers (urllib.request calling http.client calling socket, for example).
|
||||
// Once the user approves one event, every stdlib frame still on the stack for that call
|
||||
// is recorded here by (filename, function, first line) identity. A later event whose own
|
||||
// ancestor chain still contains one of those frames is the same logical action seen from
|
||||
// a deeper layer, so it is auto-approved instead of prompting again.
|
||||
bool has_approved_ancestor(const std::string& plugin_key, const std::vector<std::string>& call_site_ids) const;
|
||||
void record_approved_call_sites(const std::string& plugin_key, const std::vector<std::string>& call_site_ids);
|
||||
|
||||
bool verbose_events = true;
|
||||
|
||||
private:
|
||||
@@ -109,17 +163,19 @@ private:
|
||||
|
||||
static int audit_hook(const char* event, PyObject* args, void* user_data);
|
||||
|
||||
static thread_local std::string m_current_plugin_key;
|
||||
static thread_local std::string m_current_capability_name;
|
||||
static thread_local AuditMode m_audit_mode;
|
||||
static thread_local std::vector<boost::filesystem::path> m_scoped_allowed_roots;
|
||||
static thread_local bool m_has_last_violation;
|
||||
static thread_local AuditViolation m_last_violation;
|
||||
static thread_local std::string m_current_plugin_key;
|
||||
static thread_local std::string m_current_capability_name;
|
||||
static thread_local std::vector<AllowedRoot> m_scoped_allowed_roots;
|
||||
static thread_local bool m_audit_denial_pending;
|
||||
static thread_local bool m_has_last_violation;
|
||||
static thread_local AuditViolation m_last_violation;
|
||||
|
||||
// mutable: is_denied_filename() is a const query that must lock.
|
||||
// mutable: is_denied_filename() and has_approved_ancestor() are const queries that must lock.
|
||||
mutable std::mutex m_mutex;
|
||||
std::vector<boost::filesystem::path> m_global_allowed_roots;
|
||||
std::vector<AllowedRoot> m_global_allowed_roots;
|
||||
std::vector<std::string> m_denied_filenames;
|
||||
std::vector<std::string> m_denied_path_keywords;
|
||||
std::unordered_map<std::string, std::unordered_set<std::string>> m_approved_call_sites; // plugin_key -> call-site ids
|
||||
};
|
||||
|
||||
// RAII guard that sets the current plugin key and capability name, restoring the previous
|
||||
@@ -130,8 +186,7 @@ class ScopedPluginAuditContext
|
||||
public:
|
||||
explicit ScopedPluginAuditContext(
|
||||
const std::string& plugin_key,
|
||||
const std::string& capability_name = {},
|
||||
PluginAuditManager::AuditMode mode = PluginAuditManager::AuditMode::Loading);
|
||||
const std::string& capability_name = {});
|
||||
|
||||
~ScopedPluginAuditContext();
|
||||
|
||||
@@ -141,8 +196,7 @@ public:
|
||||
private:
|
||||
std::string m_previous_id;
|
||||
std::string m_previous_capability;
|
||||
PluginAuditManager::AuditMode m_previous_mode;
|
||||
std::vector<boost::filesystem::path> m_previous_scoped_roots;
|
||||
std::vector<AllowedRoot> m_previous_scoped_roots;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -784,6 +784,23 @@ bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstall
|
||||
parsed.plugin_name = state["plugin_name"].get<std::string>();
|
||||
if (state.contains("cloud_uuid") && state["cloud_uuid"].is_string())
|
||||
parsed.cloud_uuid = state["cloud_uuid"].get<std::string>();
|
||||
|
||||
if (state.contains("permissions") && state["permissions"].is_object()) {
|
||||
const auto& permissions = state["permissions"];
|
||||
auto read_string_list = [&permissions](const char* key, std::vector<std::string>& out) {
|
||||
if (!permissions.contains(key) || !permissions[key].is_array())
|
||||
return;
|
||||
for (const auto& entry : permissions[key])
|
||||
if (entry.is_string())
|
||||
out.push_back(entry.get<std::string>());
|
||||
};
|
||||
read_string_list("fs_read", parsed.permissions.fs_read);
|
||||
read_string_list("fs_readwrite", parsed.permissions.fs_readwrite);
|
||||
read_string_list("network_http", parsed.permissions.network_http);
|
||||
read_string_list("network_socket", parsed.permissions.network_socket);
|
||||
read_string_list("process", parsed.permissions.process);
|
||||
}
|
||||
|
||||
if (state.contains("enabled") && state["enabled"].is_boolean())
|
||||
parsed.enabled = state["enabled"].get<bool>();
|
||||
|
||||
@@ -819,6 +836,14 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
|
||||
if (!state.cloud_uuid.empty())
|
||||
json["cloud_uuid"] = state.cloud_uuid;
|
||||
|
||||
json["permissions"] = {
|
||||
{"fs_read", state.permissions.fs_read},
|
||||
{"fs_readwrite", state.permissions.fs_readwrite},
|
||||
{"network_http", state.permissions.network_http},
|
||||
{"network_socket", state.permissions.network_socket},
|
||||
{"process", state.permissions.process},
|
||||
};
|
||||
|
||||
nlohmann::json capabilities = nlohmann::json::array();
|
||||
for (const auto& [name, enabled] : state.capabilities)
|
||||
capabilities.push_back(nlohmann::json{{name, enabled}});
|
||||
@@ -836,6 +861,9 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
|
||||
const std::vector<std::pair<std::string, bool>>& capabilities)
|
||||
{
|
||||
PluginInstallState state;
|
||||
// Loading a plugin updates its lifecycle/capability state, but must retain permissions granted
|
||||
// during register_capabilities() or by a previous runtime audit prompt.
|
||||
read_install_state(plugin_dir, state);
|
||||
state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local";
|
||||
// Prefer the descriptor's recorded installed_version (the version fetched from the cloud
|
||||
// at install time, preserved across sidecar re-writes) so a stale manifest/PEP723 header
|
||||
@@ -852,10 +880,16 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
|
||||
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry)
|
||||
{
|
||||
// Install-time writer: the package is not loaded, so its capabilities are not known yet and the
|
||||
// sidecar is (re)initialized to "auto-load, nothing disabled". PluginManager writes the real
|
||||
// per-capability flags once the package is loaded, via the (dir, entry, enabled, capabilities)
|
||||
// overload.
|
||||
return write_install_state(plugin_dir, entry, true, {});
|
||||
// sidecar is (re)initialized to "auto-load, nothing disabled". This intentionally resets
|
||||
// permissions on a fresh install/reinstall. PluginManager writes the real per-capability flags
|
||||
// while preserving permissions once the package is loaded, via the overload above.
|
||||
PluginInstallState state;
|
||||
state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local";
|
||||
state.installed_version = !entry.installed_version.empty() ? entry.installed_version : entry.version;
|
||||
state.plugin_name = entry.name;
|
||||
state.cloud_uuid = entry.cloud_uuid();
|
||||
state.enabled = true;
|
||||
return write_install_state(plugin_dir, state);
|
||||
}
|
||||
|
||||
bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginDescriptor& descriptor, std::string& error)
|
||||
|
||||
@@ -82,11 +82,23 @@ inline nlohmann::json py_to_json(const pybind11::handle& o)
|
||||
return py::str(o).cast<std::string>(); // fallback: str()
|
||||
}
|
||||
|
||||
struct PluginPermissions
|
||||
{
|
||||
std::vector<std::string> fs_read;
|
||||
std::vector<std::string> fs_readwrite;
|
||||
std::vector<std::string> network_http;
|
||||
std::vector<std::string> network_socket;
|
||||
std::vector<std::string> process;
|
||||
};
|
||||
|
||||
struct PluginInstallState {
|
||||
std::string installed_from; // "local" | "cloud"
|
||||
std::string installed_version;
|
||||
std::string plugin_name;
|
||||
std::string cloud_uuid; // empty for local
|
||||
|
||||
PluginPermissions permissions;
|
||||
|
||||
bool enabled = true;
|
||||
std::vector<std::pair<std::string, bool>> capabilities; // name -> enabled, ordered
|
||||
};
|
||||
|
||||
@@ -280,7 +280,7 @@ bool load(const PluginDescriptor& descriptor,
|
||||
// (while the active plugin key is set), then instantiates each registered capability and caches
|
||||
// its get_name(). Returns one entry per capability.
|
||||
std::string bridge_error;
|
||||
auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, bridge_error);
|
||||
auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, descriptor.plugin_key, bridge_error);
|
||||
if (!bridge_error.empty()) {
|
||||
capabilities_found.clear();
|
||||
error = "Plugin registration failed: " + bridge_error;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <slic3r/plugin/PluginConfig.hpp>
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
#include <slic3r/plugin/PluginLoader.hpp>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
#include <slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp>
|
||||
@@ -622,6 +623,20 @@ std::shared_ptr<PluginCapabilityInterface> PluginManager::get_plugin_capability(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool PluginManager::get_install_state(const std::string& plugin_key, PluginInstallState& install_state)
|
||||
{
|
||||
PluginDescriptor descriptor;
|
||||
if (!try_get_plugin_descriptor(plugin_key, descriptor)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!read_install_state(boost::filesystem::path(descriptor.plugin_root), install_state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Lifecycle ───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
bool PluginManager::is_plugin_loaded(const std::string& plugin_key) const
|
||||
@@ -910,6 +925,8 @@ void PluginManager::load_plugin_impl(const std::string& plugin_key, bool skip_de
|
||||
if (!plugin_loader::load(descriptor, skip_deps, capabilities_to_enable, registry_precheck, plugin, error)) {
|
||||
if (error == LOAD_CANCELLED)
|
||||
return; // cancelled: nothing materialized survives, and no error is recorded
|
||||
if (error.rfind("Plugin registration failed:", 0) == 0)
|
||||
mark_plugin_install_state_disabled(plugin_key);
|
||||
fail(std::move(error));
|
||||
return;
|
||||
}
|
||||
@@ -1135,6 +1152,51 @@ void PluginManager::write_loaded_plugin_install_state(const std::string& plugin_
|
||||
write_install_state(boost::filesystem::path(descriptor.plugin_root), descriptor, /*enabled=*/true, capabilities);
|
||||
}
|
||||
|
||||
void PluginManager::mark_plugin_install_state_disabled(const std::string& plugin_key)
|
||||
{
|
||||
std::lock_guard<std::mutex> state_lock(m_install_state_mutex);
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
|
||||
return;
|
||||
|
||||
const boost::filesystem::path root(descriptor.plugin_root);
|
||||
PluginInstallState state;
|
||||
if (!read_install_state(root, state)) {
|
||||
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
|
||||
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
|
||||
state.plugin_name = descriptor.name;
|
||||
state.cloud_uuid = descriptor.cloud_uuid();
|
||||
}
|
||||
state.enabled = false;
|
||||
if (!write_install_state(root, state))
|
||||
return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (Plugin* plugin = find_plugin_locked(plugin_key))
|
||||
plugin->descriptor.enabled = false;
|
||||
}
|
||||
|
||||
void PluginManager::revoke_plugin_permissions(const std::string& plugin_key)
|
||||
{
|
||||
std::lock_guard<std::mutex> state_lock(m_install_state_mutex);
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
|
||||
return;
|
||||
|
||||
const boost::filesystem::path root(descriptor.plugin_root);
|
||||
PluginInstallState state;
|
||||
if (!read_install_state(root, state)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to read install state for " << plugin_key;
|
||||
return;
|
||||
}
|
||||
|
||||
state.permissions = {};
|
||||
if (!write_install_state(root, state))
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to revoke permissions for " << plugin_key;
|
||||
}
|
||||
|
||||
// ── Callbacks ───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void PluginManager::subscribe_on_load_callback(PluginLifecycleCompleteFn fn)
|
||||
@@ -1354,6 +1416,11 @@ bool PluginManager::install_plugin(const boost::filesystem::path& filepath, Plug
|
||||
return false;
|
||||
}
|
||||
|
||||
// Every successful install may have replaced executable plugin code. Revoke any permissions
|
||||
// associated with the previous package so the newly installed version must request them again.
|
||||
if (!plugin_descriptor.plugin_key.empty())
|
||||
revoke_plugin_permissions(plugin_descriptor.plugin_key);
|
||||
|
||||
if (!plugin_descriptor.plugin_key.empty())
|
||||
clear_plugin_error(plugin_descriptor.plugin_key);
|
||||
|
||||
@@ -1741,6 +1808,7 @@ bool PluginManager::update_cloud_plugin(const std::string& plugin_key, std::stri
|
||||
}
|
||||
|
||||
clear_plugin_error(plugin_key);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +159,8 @@ public:
|
||||
PluginCapabilityType type = PluginCapabilityType::Unknown,
|
||||
bool only_enabled = true) const;
|
||||
|
||||
bool get_install_state(const std::string& plugin_key, PluginInstallState& install_state);
|
||||
|
||||
void load_plugin(const std::string& plugin_key, bool skip_deps = false, std::vector<std::string> capabilities_to_enable = {});
|
||||
bool unload_plugin(const std::string& plugin_key);
|
||||
void unload_all_plugins();
|
||||
@@ -255,6 +257,9 @@ private:
|
||||
|
||||
// Writes the sidecar for a loaded plugin (enabled=true plus the current per-capability flags).
|
||||
void write_loaded_plugin_install_state(const std::string& plugin_key);
|
||||
void mark_plugin_install_state_disabled(const std::string& plugin_key);
|
||||
// Revoke permissions after a package replacement so the new package must request them again.
|
||||
void revoke_plugin_permissions(const std::string& plugin_key);
|
||||
|
||||
bool finalize_cloud_plugin_removal(const PluginDescriptor& plugin, bool keep_local, std::string& error);
|
||||
bool delete_installed_plugin_package(const PluginDescriptor& plugin, std::string& error);
|
||||
|
||||
@@ -26,25 +26,30 @@
|
||||
try { \
|
||||
override_call; \
|
||||
} catch (pybind11::error_already_set & err) { \
|
||||
const bool _orca_audit_denial = ::Slic3r::PluginAuditManager::instance().audit_denial_pending(); \
|
||||
if (_orca_audit_denial) \
|
||||
::Slic3r::PluginAuditManager::instance().clear_current_plugin(); \
|
||||
::Slic3r::log_python_exception_keep(err); \
|
||||
if (_orca_audit_denial) \
|
||||
::Slic3r::PluginAuditManager::instance().clear_audit_denial(); \
|
||||
throw; \
|
||||
}
|
||||
|
||||
// Opens the plugin's filesystem audit scope for the duration of a C++ -> Python call, and publishes
|
||||
// the calling capability's cached name so host APIs invoked from Python can tell which capability
|
||||
// they are serving. No-op without an audit plugin key. Declares a local `_orca_audit_scope`.
|
||||
#define ORCA_PY_AUDIT_SCOPE(mode) \
|
||||
#define ORCA_PY_AUDIT_SCOPE() \
|
||||
std::optional<::Slic3r::ScopedPluginAuditContext> _orca_audit_scope; \
|
||||
if (const std::string& _orca_audit_key = this->audit_plugin_key(); !_orca_audit_key.empty()) \
|
||||
_orca_audit_scope.emplace(_orca_audit_key, this->name(), mode)
|
||||
_orca_audit_scope.emplace(_orca_audit_key, this->name())
|
||||
|
||||
#define ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, ...) \
|
||||
#define ORCA_PY_OVERRIDE_AUDITED(audit_setup, override_macro, ret, base, name, ...) \
|
||||
do { \
|
||||
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this); \
|
||||
::Slic3r::PythonGILState _orca_python_gil; \
|
||||
if (!_orca_python_gil) \
|
||||
throw std::runtime_error("Python interpreter is shutting down"); \
|
||||
ORCA_PY_AUDIT_SCOPE(mode); \
|
||||
ORCA_PY_AUDIT_SCOPE(); \
|
||||
if (_orca_audit_scope) \
|
||||
audit_setup(); \
|
||||
ORCA_PY_LOGGED_OVERRIDE_BODY(override_macro(ret, base, name, ##__VA_ARGS__)); \
|
||||
@@ -58,7 +63,7 @@ public:
|
||||
|
||||
std::string get_name() const override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name);
|
||||
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name);
|
||||
}
|
||||
|
||||
// Config UI hooks. Available on every capability type, so they live here rather than in
|
||||
@@ -66,7 +71,6 @@ public:
|
||||
bool has_config_ui() const override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE,
|
||||
bool,
|
||||
@@ -77,7 +81,6 @@ public:
|
||||
std::string get_config_ui() const override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE,
|
||||
std::string,
|
||||
@@ -94,7 +97,7 @@ public:
|
||||
// mistake), both fall back to the base's empty object rather than writing `"cap_config": null`.
|
||||
nlohmann::json get_default_config() const override
|
||||
{
|
||||
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
|
||||
ORCA_PY_AUDIT_SCOPE();
|
||||
try {
|
||||
pybind11::gil_scoped_acquire gil;
|
||||
pybind11::function override = pybind11::get_override(static_cast<const Base*>(this), "get_default_config");
|
||||
@@ -117,17 +120,17 @@ public:
|
||||
|
||||
void on_load() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_load);
|
||||
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_load);
|
||||
}
|
||||
|
||||
void on_unload() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_unload);
|
||||
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_unload);
|
||||
}
|
||||
|
||||
void on_cancelled() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled);
|
||||
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,7 +142,7 @@ public:
|
||||
PluginCapabilityType get_type() const override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface,
|
||||
[] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface,
|
||||
get_type);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,6 +87,12 @@ void log_python_exception_keep(pybind11::error_already_set& err)
|
||||
if (!gil)
|
||||
return;
|
||||
|
||||
// Traceback output is host-owned work. In particular, the stderr tee opens the Python log
|
||||
// file on every write. Keep that open outside the plugin audit context, otherwise an ordinary
|
||||
// exception raised by a plugin can recursively trigger the filesystem permission dialog while
|
||||
// its original exception is being reported.
|
||||
ScopedPluginAuditContext audit_suppression("");
|
||||
|
||||
// Non-destructive: print the traceback to sys.stderr (tee'd to the session log)
|
||||
// WITHOUT consuming err, so the caller can rethrow it intact. For example, downstream C++
|
||||
// catchers can still read err.what() for the user-facing dialog. We must NOT use
|
||||
@@ -622,10 +628,6 @@ bool PythonInterpreter::initialize()
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(info) << "Bundled uv executable not found";
|
||||
|
||||
// Install the CPython audit hook for plugin policy enforcement.
|
||||
// This is defense-in-depth: today it only inspects the `open` audit event
|
||||
// and blocks writes outside the allowed roots; subprocess/socket/ctypes and
|
||||
// other events are not yet handled. It is NOT a full security sandbox.
|
||||
PluginAuditManager::instance().install_hook();
|
||||
|
||||
// Persist Python stderr (plugin tracebacks, including uncaught
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "PythonPluginBridge.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
@@ -40,6 +41,7 @@ thread_local std::string g_active_plugin_key;
|
||||
std::mutex g_registry_mutex;
|
||||
std::unordered_map<std::string, std::vector<py::object>> g_pending_capabilities;
|
||||
std::unordered_map<std::string, py::object> g_pending_package;
|
||||
std::unordered_map<std::string, std::vector<std::string>> g_pending_fs_read_permissions;
|
||||
struct PluginInstanceHandle
|
||||
{
|
||||
// The C++ plugin interface points into a Python object. Keep both alive through one
|
||||
@@ -77,6 +79,8 @@ void discard_pending_capture_without_python(const std::string& plugin_key)
|
||||
(void) package->second.release();
|
||||
g_pending_package.erase(package);
|
||||
}
|
||||
|
||||
g_pending_fs_read_permissions.erase(plugin_key);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -101,34 +105,38 @@ void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
|
||||
// for this same entry path.
|
||||
g_pending_capabilities.erase(plugin_key);
|
||||
g_pending_package.erase(plugin_key);
|
||||
g_pending_fs_read_permissions.erase(plugin_key);
|
||||
}
|
||||
// From now until finalize/cancel, @orca.plugin and register_capability() calls made by
|
||||
// Python code on this thread are attributed to this plugin.
|
||||
g_active_plugin_key = plugin_key;
|
||||
}
|
||||
|
||||
std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& plugin_key, std::string& error)
|
||||
std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& capture_key,
|
||||
const std::string& plugin_key,
|
||||
std::string& error)
|
||||
{
|
||||
PythonGILState gil;
|
||||
if (!gil) {
|
||||
error = "Python interpreter is shutting down";
|
||||
return {};
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << plugin_key;
|
||||
BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << capture_key;
|
||||
|
||||
// Phase 1: run the package class's register_capabilities() while the active key is
|
||||
// still set. That method is expected to call orca.register_capability() once per
|
||||
// capability class, and register_capability() needs g_active_plugin_key to know which
|
||||
// pending bucket to append to.
|
||||
{
|
||||
auto clear_active_key = [&plugin_key]() {
|
||||
if (g_active_plugin_key == plugin_key)
|
||||
auto clear_active_key = [&capture_key]() {
|
||||
if (g_active_plugin_key == capture_key)
|
||||
g_active_plugin_key.clear();
|
||||
};
|
||||
auto discard_pending_for_key = [&plugin_key]() {
|
||||
auto discard_pending_for_key = [&capture_key]() {
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
g_pending_capabilities.erase(plugin_key);
|
||||
g_pending_package.erase(plugin_key);
|
||||
g_pending_capabilities.erase(capture_key);
|
||||
g_pending_package.erase(capture_key);
|
||||
g_pending_fs_read_permissions.erase(capture_key);
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -138,7 +146,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
|
||||
py::object package_cls;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
auto it = g_pending_package.find(plugin_key);
|
||||
auto it = g_pending_package.find(capture_key);
|
||||
if (it != g_pending_package.end()) {
|
||||
package_cls = it->second;
|
||||
g_pending_package.erase(it);
|
||||
@@ -157,6 +165,33 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
|
||||
// are kept.
|
||||
py::object package = package_cls();
|
||||
package.attr("register_capabilities")();
|
||||
|
||||
// Permission declarations are collected while register_capabilities() runs so the
|
||||
// plugin can describe its needs without touching wx from the Python load worker. Ask
|
||||
// only after registration returns, before capability instances are materialized.
|
||||
std::vector<std::string> fs_read_permissions;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
auto it = g_pending_fs_read_permissions.find(capture_key);
|
||||
if (it != g_pending_fs_read_permissions.end()) {
|
||||
fs_read_permissions = std::move(it->second);
|
||||
g_pending_fs_read_permissions.erase(it);
|
||||
}
|
||||
}
|
||||
if (!fs_read_permissions.empty()) {
|
||||
bool granted = false;
|
||||
{
|
||||
py::gil_scoped_release release;
|
||||
granted = PluginAuditManager::instance().request_filesystem_read_permissions(plugin_key, fs_read_permissions);
|
||||
}
|
||||
if (!granted) {
|
||||
error = "Plugin filesystem read permission request denied";
|
||||
BOOST_LOG_TRIVIAL(warning) << error << " for key " << plugin_key;
|
||||
discard_pending_for_key();
|
||||
clear_active_key();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
} catch (py::error_already_set& err) {
|
||||
log_python_exception_keep(err);
|
||||
error = err.what();
|
||||
@@ -180,7 +215,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
|
||||
std::vector<py::object> classes;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
auto it = g_pending_capabilities.find(plugin_key);
|
||||
auto it = g_pending_capabilities.find(capture_key);
|
||||
if (it != g_pending_capabilities.end()) {
|
||||
classes = std::move(it->second);
|
||||
g_pending_capabilities.erase(it);
|
||||
@@ -189,10 +224,10 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
|
||||
|
||||
// Registration is complete. Later register_capability() calls should fail instead of
|
||||
// accidentally attaching themselves to this plugin.
|
||||
if (g_active_plugin_key == plugin_key)
|
||||
if (g_active_plugin_key == capture_key)
|
||||
g_active_plugin_key.clear();
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << plugin_key;
|
||||
BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << capture_key;
|
||||
|
||||
std::vector<CapturedCapability> capabilities;
|
||||
capabilities.reserve(classes.size());
|
||||
@@ -204,7 +239,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
|
||||
py::object instance = cls();
|
||||
if (!py::isinstance<PluginCapabilityInterface>(instance)) {
|
||||
error = "Registered capability must inherit from a PluginCapability base";
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << plugin_key
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << capture_key
|
||||
<< " error=" << error;
|
||||
return {};
|
||||
}
|
||||
@@ -212,7 +247,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
|
||||
auto capability_iface = instance.cast<std::shared_ptr<PluginCapabilityInterface>>();
|
||||
if (!capability_iface) {
|
||||
error = "Failed to cast Python capability to PluginCapabilityInterface";
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << plugin_key
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << capture_key
|
||||
<< " error=" << error;
|
||||
return {};
|
||||
}
|
||||
@@ -226,7 +261,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
|
||||
// here is a hard error that rejects the whole plugin capture.
|
||||
if (name.find(';') != std::string::npos) {
|
||||
error = "Capability name must not contain ';': " + name;
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << plugin_key
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << capture_key
|
||||
<< " error=" << error;
|
||||
return {};
|
||||
}
|
||||
@@ -246,12 +281,12 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
|
||||
// log the traceback here. GIL is held for the duration of finalize_plugin_capture.
|
||||
log_python_exception_keep(err);
|
||||
error = err.what();
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << plugin_key
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << capture_key
|
||||
<< " error=" << error;
|
||||
return {};
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << plugin_key
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << capture_key
|
||||
<< " error=" << error;
|
||||
return {};
|
||||
}
|
||||
@@ -279,6 +314,7 @@ void PythonPluginBridge::cancel_plugin_capture(const std::string& plugin_key)
|
||||
// may already have registered under this key.
|
||||
g_pending_capabilities.erase(plugin_key);
|
||||
g_pending_package.erase(plugin_key);
|
||||
g_pending_fs_read_permissions.erase(plugin_key);
|
||||
}
|
||||
|
||||
if (g_active_plugin_key == plugin_key)
|
||||
@@ -305,6 +341,7 @@ void PythonPluginBridge::clear_pending_captures()
|
||||
(void) pkg.release();
|
||||
}
|
||||
g_pending_package.clear();
|
||||
g_pending_fs_read_permissions.clear();
|
||||
g_active_plugin_key.clear();
|
||||
return;
|
||||
}
|
||||
@@ -313,6 +350,7 @@ void PythonPluginBridge::clear_pending_captures()
|
||||
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)";
|
||||
g_pending_capabilities.clear();
|
||||
g_pending_package.clear();
|
||||
g_pending_fs_read_permissions.clear();
|
||||
g_active_plugin_key.clear();
|
||||
}
|
||||
|
||||
@@ -450,6 +488,27 @@ void bind_python_api(pybind11::module_& m)
|
||||
},
|
||||
R"pbdoc(Register a PluginCapability subclass while OrcaSlicer loads your module.)pbdoc");
|
||||
|
||||
m.def(
|
||||
"request_permissions",
|
||||
[](const std::vector<std::string>& fs_read) {
|
||||
if (g_active_plugin_key.empty())
|
||||
throw py::value_error("request_permissions() called outside plugin discovery context");
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
auto& requested = g_pending_fs_read_permissions[g_active_plugin_key];
|
||||
for (const std::string& path : fs_read) {
|
||||
if (path.empty())
|
||||
throw py::value_error("request_permissions(fs_read=...) does not accept empty paths");
|
||||
if (std::find(requested.begin(), requested.end(), path) == requested.end())
|
||||
requested.push_back(path);
|
||||
}
|
||||
},
|
||||
py::arg("fs_read") = std::vector<std::string>{},
|
||||
R"pbdoc(Request filesystem read permissions while OrcaSlicer loads your module.
|
||||
|
||||
The host presents the request to the user after register_capabilities() returns. Denying the
|
||||
request aborts the plugin load.)pbdoc");
|
||||
|
||||
m.def("plugin", [](py::object cls) {
|
||||
if (g_active_plugin_key.empty())
|
||||
throw py::value_error("@orca.plugin used outside plugin discovery context");
|
||||
|
||||
@@ -31,7 +31,7 @@ public:
|
||||
// Returns one CapturedCapability per capability, or an empty vector on failure
|
||||
// (error message populated).
|
||||
std::vector<CapturedCapability> finalize_plugin_capture(
|
||||
const std::string& plugin_key, std::string& error);
|
||||
const std::string& capture_key, const std::string& plugin_key, std::string& error);
|
||||
|
||||
// Clear any pending registrations for the key. Safe to call when import fails.
|
||||
void cancel_plugin_capture(const std::string& plugin_key);
|
||||
|
||||
@@ -16,7 +16,6 @@ public:
|
||||
std::string get_icon() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE,
|
||||
std::string,
|
||||
@@ -27,7 +26,6 @@ public:
|
||||
std::string get_ui() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE_PURE,
|
||||
std::string,
|
||||
@@ -42,7 +40,7 @@ public:
|
||||
if (!gil)
|
||||
throw std::runtime_error("Python interpreter is shutting down");
|
||||
|
||||
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
|
||||
ORCA_PY_AUDIT_SCOPE();
|
||||
|
||||
pybind11::function override = pybind11::get_override(static_cast<PagesPluginCapability*>(this), "on_message");
|
||||
if (!override)
|
||||
|
||||
@@ -16,206 +16,206 @@ public:
|
||||
AgentInfo get_agent_info() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability,
|
||||
get_agent_info);
|
||||
}
|
||||
|
||||
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id,
|
||||
dev_ip, username, password, use_ssl);
|
||||
}
|
||||
|
||||
int disconnect_printer() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer);
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer);
|
||||
}
|
||||
|
||||
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id,
|
||||
json_str, qos, flag);
|
||||
}
|
||||
|
||||
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer,
|
||||
dev_id, json_str, qos, flag);
|
||||
}
|
||||
|
||||
bool start_discovery(bool start, bool sending) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start,
|
||||
sending);
|
||||
}
|
||||
|
||||
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip,
|
||||
sec_link, detect);
|
||||
}
|
||||
|
||||
std::string get_user_selected_machine() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability,
|
||||
get_user_selected_machine);
|
||||
}
|
||||
|
||||
int set_user_selected_machine(std::string dev_id) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
|
||||
set_user_selected_machine, dev_id);
|
||||
}
|
||||
|
||||
int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
|
||||
start_send_gcode_to_sdcard, params, update_fn, cancel_fn, wait_fn);
|
||||
}
|
||||
|
||||
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print,
|
||||
params, update_fn, cancel_fn);
|
||||
}
|
||||
|
||||
FilamentSyncMode get_filament_sync_mode() const override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability,
|
||||
get_filament_sync_mode);
|
||||
}
|
||||
|
||||
bool fetch_filament_info(std::string dev_id) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id);
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id);
|
||||
}
|
||||
|
||||
int check_cert() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert);
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert);
|
||||
}
|
||||
|
||||
void install_device_cert(std::string dev_id, bool lan_only) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id,
|
||||
lan_only);
|
||||
}
|
||||
|
||||
int ping_bind(std::string ping_code) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ping_bind, ping_code);
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ping_bind, ping_code);
|
||||
}
|
||||
|
||||
int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id,
|
||||
dev_model, sec_link, timezone, improved, update_fn);
|
||||
}
|
||||
|
||||
int unbind(std::string dev_id) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, unbind, dev_id);
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, unbind, dev_id);
|
||||
}
|
||||
|
||||
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params,
|
||||
update_fn, cancel_fn, wait_fn);
|
||||
}
|
||||
|
||||
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
|
||||
start_local_print_with_record, params, update_fn, cancel_fn, wait_fn);
|
||||
}
|
||||
|
||||
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params,
|
||||
update_fn, cancel_fn);
|
||||
}
|
||||
|
||||
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id,
|
||||
file_name, callback);
|
||||
}
|
||||
|
||||
int set_server_callback(OnServerErrFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn);
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn);
|
||||
}
|
||||
|
||||
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn);
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn);
|
||||
}
|
||||
|
||||
int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn,
|
||||
fn);
|
||||
}
|
||||
|
||||
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn,
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn,
|
||||
fn);
|
||||
}
|
||||
|
||||
int set_on_message_fn(OnMessageFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn);
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn);
|
||||
}
|
||||
|
||||
int set_on_user_message_fn(OnMessageFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn);
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn);
|
||||
}
|
||||
|
||||
int set_on_local_connect_fn(OnLocalConnectedFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn);
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn);
|
||||
}
|
||||
|
||||
int set_on_local_message_fn(OnMessageFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn);
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn);
|
||||
}
|
||||
|
||||
int set_queue_on_main_fn(QueueOnMainFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn);
|
||||
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn);
|
||||
}
|
||||
|
||||
// request_bind_ticket returns its ticket through a std::string* out-param, which pybind11
|
||||
@@ -223,7 +223,7 @@ public:
|
||||
// returns a (result, ticket) tuple, which we unpack into the int result and the out-param.
|
||||
int request_bind_ticket(std::string* ticket) override
|
||||
{
|
||||
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
|
||||
ORCA_PY_AUDIT_SCOPE();
|
||||
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this);
|
||||
::Slic3r::PythonGILState gil;
|
||||
if (!gil)
|
||||
|
||||
@@ -13,7 +13,6 @@ public:
|
||||
ExecutionResult execute() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE_PURE,
|
||||
ExecutionResult,
|
||||
|
||||
@@ -10,7 +10,6 @@ public:
|
||||
using PyPluginCommonTrampoline<SlicingPipelinePluginCapability>::PyPluginCommonTrampoline;
|
||||
ExecutionResult execute(SlicingPipelineContext& ctx) override {
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[&]{
|
||||
// At Step.psGCodePostProcess the plugin edits the exported G-code file, which lives
|
||||
// outside data_dir() (a temp/output folder), so writing to it would otherwise be
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,6 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_mutable_polygon.cpp
|
||||
test_mutable_priority_queue.cpp
|
||||
test_nozzle_volume_type.cpp
|
||||
test_step.cpp
|
||||
test_stl.cpp
|
||||
test_triangle_selector.cpp
|
||||
test_meshboolean.cpp
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Format/STEP.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
static void write_step_line(const std::string &path, const std::string &line)
|
||||
{
|
||||
boost::nowide::ofstream file(path, std::ios::binary);
|
||||
file << "ISO-10303-21;\n" << line << "\nEND-ISO-10303-21;\n";
|
||||
}
|
||||
|
||||
// preprocess() hands back the input path unless it transcoded into a temporary.
|
||||
static std::string preprocess_result(const std::string &line)
|
||||
{
|
||||
ScopedSlic3rTemporaryDir scratch;
|
||||
|
||||
ScopedTemporaryFile step(".step");
|
||||
write_step_line(step.string(), line);
|
||||
|
||||
std::string output_path;
|
||||
StepPreProcessor preprocessor;
|
||||
REQUIRE(preprocessor.preprocess(step.string().c_str(), output_path));
|
||||
|
||||
return output_path == step.string() ? "untouched" : "transcoded";
|
||||
}
|
||||
|
||||
// data/utf8_part_names.step is three boxes written by OCCT's own STEP writer, whose
|
||||
// PRODUCT names were then patched to raw UTF-8. Most CAD exporters write non-ASCII names
|
||||
// that way rather than in the \X2\ escape form. The third part is ASCII, as a control.
|
||||
TEST_CASE("Part names with multi-byte UTF-8 survive import", "[Step]")
|
||||
{
|
||||
// getNamedSolids() replaces a name that isUtf8() rejects with a running number.
|
||||
const std::string path = TEST_DATA_DIR PATH_SEPARATOR "utf8_part_names.step";
|
||||
|
||||
Model model;
|
||||
bool cancel = false;
|
||||
Step step(path); // no isUtf8Fn, matching how Model::read_from_step builds it
|
||||
|
||||
REQUIRE(step.load() == Step::Step_Status::LOAD_SUCCESS);
|
||||
REQUIRE(step.mesh(&model, cancel, false) == Step::Step_Status::MESH_SUCCESS);
|
||||
|
||||
REQUIRE(model.objects.size() == 1);
|
||||
const ModelObject *object = model.objects.front();
|
||||
REQUIRE(object->volumes.size() == 3);
|
||||
// "ce" is split off, or the hex escape would swallow it as further hex digits.
|
||||
CHECK(object->volumes[0]->name == "pi\xC3\xA8" "ce");
|
||||
CHECK(object->volumes[1]->name == "Geh\xC3\xA4use");
|
||||
CHECK(object->volumes[2]->name == "bracket");
|
||||
}
|
||||
|
||||
TEST_CASE("isUtf8 recognises two, three and four byte sequences", "[Step]")
|
||||
{
|
||||
CHECK(StepPreProcessor::isUtf8("\xC3\xA9")); // U+00E9
|
||||
CHECK(StepPreProcessor::isUtf8("\xE4\xB8\xAD")); // U+4E2D
|
||||
CHECK(StepPreProcessor::isUtf8("\xF0\x9F\x94\xA9")); // U+1F529
|
||||
CHECK_FALSE(StepPreProcessor::isUtf8("\x81\x30")); // 0x81 is not a lead byte
|
||||
CHECK_FALSE(StepPreProcessor::isUtf8("\xC3")); // truncated sequence
|
||||
}
|
||||
|
||||
// The only caller of isGBK is preprocess(), which nothing calls today.
|
||||
TEST_CASE("Encoding detection decides whether a step file is transcoded", "[Step]")
|
||||
{
|
||||
SECTION("UTF-8, so left alone")
|
||||
{
|
||||
// A two byte sequence also satisfies every GBK range, so misdetecting it as
|
||||
// not-UTF-8 sends it to be transcoded.
|
||||
const std::string sequence = GENERATE(std::string("\xC3\xA9"), // U+00E9
|
||||
std::string("\xE4\xB8\xAD"), // U+4E2D
|
||||
std::string("\xF0\x9F\x94\xA9")); // U+1F529
|
||||
|
||||
CHECK(preprocess_result("NAME('" + sequence + "');") == "untouched");
|
||||
}
|
||||
|
||||
SECTION("neither UTF-8 nor GBK, so left alone")
|
||||
{
|
||||
// 0x81 is not a UTF-8 lead byte, and 0x30 is below the 0x40 floor for a GBK trail.
|
||||
CHECK(preprocess_result("NAME('\x81\x30');") == "untouched");
|
||||
}
|
||||
|
||||
SECTION("GBK, so transcoded")
|
||||
{
|
||||
// U+554A in GBK, whose lead byte is not valid UTF-8. Pins the other direction,
|
||||
// since a detector that never reports GBK would pass every case above.
|
||||
CHECK(preprocess_result("NAME('\xB0\xA1');") == "transcoded");
|
||||
}
|
||||
|
||||
SECTION("plain ASCII, so left alone") { CHECK(preprocess_result("NAME('bracket');") == "untouched"); }
|
||||
}
|
||||
@@ -34,4 +34,24 @@ struct ScopedDataDir
|
||||
ScopedDataDir& operator=(const ScopedDataDir&) = delete;
|
||||
};
|
||||
|
||||
// Point resources_dir() at a throwaway directory for the lifetime of a test and restore the
|
||||
// previous value afterwards, mirroring ScopedDataDir.
|
||||
struct ScopedResourcesDir
|
||||
{
|
||||
ScopedTemporaryDir tmp;
|
||||
boost::filesystem::path dir;
|
||||
std::string previous;
|
||||
|
||||
explicit ScopedResourcesDir(const std::string& tag)
|
||||
: tmp("orca-" + tag), dir(tmp.path()), previous(resources_dir())
|
||||
{
|
||||
set_resources_dir(dir.string());
|
||||
}
|
||||
|
||||
~ScopedResourcesDir() { set_resources_dir(previous); }
|
||||
|
||||
ScopedResourcesDir(const ScopedResourcesDir&) = delete;
|
||||
ScopedResourcesDir& operator=(const ScopedResourcesDir&) = delete;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -27,6 +27,16 @@ void seed_denied_names()
|
||||
mgr.add_denied_filename(name);
|
||||
}
|
||||
|
||||
// Seed the keyword registry with the same list install_hook() uses. Same rationale as
|
||||
// seed_denied_names(): a process-singleton registry, seeded from the single shared source so
|
||||
// production and tests cannot drift apart.
|
||||
void seed_denied_keywords()
|
||||
{
|
||||
PluginAuditManager& mgr = PluginAuditManager::instance();
|
||||
for (const auto& keyword : PluginAuditManager::default_denied_path_keywords())
|
||||
mgr.add_denied_path_keyword(keyword);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit]")
|
||||
@@ -81,7 +91,7 @@ TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption", "[audit]")
|
||||
TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
|
||||
{
|
||||
ScopedDataDir data_dir_guard("plugin-audit-deny");
|
||||
seed_denied_names();
|
||||
@@ -91,8 +101,8 @@ TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption"
|
||||
// config and the token would otherwise be reachable simply by living inside it.
|
||||
mgr.add_global_allowed_root(data_dir());
|
||||
|
||||
// Enter a plugin context. The deny must hold in Loading mode, which every scope runs in.
|
||||
ScopedPluginAuditContext ctx("test_plugin", "", PluginAuditManager::AuditMode::Loading);
|
||||
// Enter a plugin context. The deny must hold even inside a globally allowed root.
|
||||
ScopedPluginAuditContext ctx("test_plugin", "");
|
||||
|
||||
const fs::path conf = fs::path(data_dir()) / (SLIC3R_APP_KEY ".conf");
|
||||
const fs::path token = fs::path(data_dir()) / secret_constants::USER_SECRET_FILENAME;
|
||||
@@ -103,6 +113,13 @@ TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption"
|
||||
CHECK(decision.allowed);
|
||||
}
|
||||
|
||||
SECTION("a file outside the allowed root is blocked for reads as well as writes")
|
||||
{
|
||||
AuditDecision decision = mgr.check_open((fs::path(data_dir()).parent_path() / "outside.txt").string(), "r");
|
||||
CHECK_FALSE(decision.allowed);
|
||||
CHECK(decision.reason == "outside allowed root");
|
||||
}
|
||||
|
||||
SECTION("writing the app config is blocked despite data_dir() being allowed")
|
||||
{
|
||||
AuditDecision decision = mgr.check_open(conf.string(), "w");
|
||||
@@ -110,16 +127,14 @@ TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption"
|
||||
CHECK(decision.reason == "denied filename");
|
||||
}
|
||||
|
||||
SECTION("reading the app config is blocked even though Loading exempts reads")
|
||||
SECTION("reading the app config is blocked despite the allowed root")
|
||||
{
|
||||
// Without the deny, a read in Loading mode short-circuits to allow. The deny sits above
|
||||
// that exemption, so this must still be blocked.
|
||||
AuditDecision decision = mgr.check_open(conf.string(), "r");
|
||||
CHECK_FALSE(decision.allowed);
|
||||
CHECK(decision.reason == "denied filename");
|
||||
}
|
||||
|
||||
SECTION("reading the cloud refresh token is blocked in Loading mode")
|
||||
SECTION("reading the cloud refresh token is blocked")
|
||||
{
|
||||
AuditDecision decision = mgr.check_open(token.string(), "r");
|
||||
CHECK_FALSE(decision.allowed);
|
||||
@@ -150,7 +165,7 @@ TEST_CASE("Plugin audit deny beats a plugin's own scoped root", "[audit]")
|
||||
const fs::path plugin_dir = fs::path(data_dir()) / "plugins" / "test_plugin";
|
||||
fs::create_directories(plugin_dir);
|
||||
|
||||
ScopedPluginAuditContext ctx("test_plugin", "", PluginAuditManager::AuditMode::Loading);
|
||||
ScopedPluginAuditContext ctx("test_plugin", "");
|
||||
mgr.add_scoped_allowed_root(plugin_dir);
|
||||
|
||||
SECTION("the plugin's own non-denied file opens for read and write")
|
||||
@@ -185,3 +200,139 @@ TEST_CASE("Plugin audit does not constrain non-plugin code", "[audit]")
|
||||
CHECK(mgr.check_open(conf.string(), "w").allowed);
|
||||
CHECK(mgr.check_open(conf.string(), "r").allowed);
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin audit denies secret/certificate/config-like paths by keyword", "[audit]")
|
||||
{
|
||||
seed_denied_keywords();
|
||||
const PluginAuditManager& mgr = PluginAuditManager::instance();
|
||||
|
||||
SECTION("a 'secrets' directory component is denied")
|
||||
{
|
||||
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/secrets/api_key.json")));
|
||||
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/secret/token.txt")));
|
||||
}
|
||||
|
||||
SECTION("a 'certificate(s)' directory component is denied")
|
||||
{
|
||||
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/cert/slicer_base64.cer")));
|
||||
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/certificates/ca.pem")));
|
||||
}
|
||||
|
||||
SECTION("a 'conf'/'config' directory or file component is denied")
|
||||
{
|
||||
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/conf/settings.json")));
|
||||
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/config/settings.json")));
|
||||
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/plugin.conf")));
|
||||
}
|
||||
|
||||
SECTION("matching is case-insensitive")
|
||||
{
|
||||
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/SECRETS/token.txt")));
|
||||
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/CertBundle/ca.pem")));
|
||||
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/CONFIG.JSON")));
|
||||
}
|
||||
|
||||
SECTION("matching is not limited to the base name -- any ancestor component counts")
|
||||
{
|
||||
CHECK(mgr.is_denied_path_keyword(fs::path("/data/secrets/nested/deep/file.txt")));
|
||||
}
|
||||
|
||||
SECTION("an unrelated path is not denied")
|
||||
{
|
||||
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/output/model.gcode")));
|
||||
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/storage/state.json")));
|
||||
}
|
||||
|
||||
SECTION("an empty path is not denied")
|
||||
{
|
||||
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin audit is_denied_path combines the filename and keyword registries", "[audit]")
|
||||
{
|
||||
seed_denied_names();
|
||||
seed_denied_keywords();
|
||||
const PluginAuditManager& mgr = PluginAuditManager::instance();
|
||||
|
||||
SECTION("a filename-registry match is denied")
|
||||
{
|
||||
CHECK(mgr.is_denied_path(fs::path(SLIC3R_APP_KEY ".conf")));
|
||||
}
|
||||
|
||||
SECTION("a keyword-registry match is denied")
|
||||
{
|
||||
CHECK(mgr.is_denied_path(fs::path("/plugin/secrets/token.txt")));
|
||||
}
|
||||
|
||||
SECTION("a path matching neither registry is not denied")
|
||||
{
|
||||
CHECK_FALSE(mgr.is_denied_path(fs::path("/plugin/output/model.gcode")));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin audit a read-only allowed root blocks writes but not reads", "[audit]")
|
||||
{
|
||||
ScopedDataDir data_dir_guard("plugin-audit-readonly");
|
||||
ScopedResourcesDir resources_dir_guard("plugin-audit-readonly-resources");
|
||||
seed_denied_names();
|
||||
seed_denied_keywords();
|
||||
|
||||
PluginAuditManager& mgr = PluginAuditManager::instance();
|
||||
mgr.add_global_allowed_root(resources_dir(), /*allow_write=*/false);
|
||||
|
||||
ScopedPluginAuditContext ctx("test_plugin", "");
|
||||
|
||||
const fs::path readonly_file = fs::path(resources_dir()) / "profiles" / "vendor.json";
|
||||
|
||||
SECTION("a read inside the read-only root is allowed")
|
||||
{
|
||||
CHECK(mgr.check_open(readonly_file.string(), "r").allowed);
|
||||
}
|
||||
|
||||
SECTION("a write inside the read-only root is blocked")
|
||||
{
|
||||
AuditDecision decision = mgr.check_open(readonly_file.string(), "w");
|
||||
CHECK_FALSE(decision.allowed);
|
||||
CHECK(decision.reason == "outside allowed root");
|
||||
}
|
||||
|
||||
SECTION("a create inside the read-only root is blocked")
|
||||
{
|
||||
AuditDecision decision = mgr.check_path_access(readonly_file, /*is_write=*/true);
|
||||
CHECK_FALSE(decision.allowed);
|
||||
}
|
||||
|
||||
SECTION("the bundled cert underneath the read-only root is denied even for reads")
|
||||
{
|
||||
const fs::path cert = fs::path(resources_dir()) / "cert" / "slicer_base64.cer";
|
||||
AuditDecision decision = mgr.check_open(cert.string(), "r");
|
||||
CHECK_FALSE(decision.allowed);
|
||||
CHECK(decision.reason == "denied path keyword");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin audit a scoped root can also be registered read-only", "[audit]")
|
||||
{
|
||||
ScopedDataDir data_dir_guard("plugin-audit-scoped-readonly");
|
||||
seed_denied_names();
|
||||
seed_denied_keywords();
|
||||
|
||||
PluginAuditManager& mgr = PluginAuditManager::instance();
|
||||
|
||||
const fs::path readonly_dir = fs::path(data_dir()) / "readonly_scope";
|
||||
fs::create_directories(readonly_dir);
|
||||
|
||||
ScopedPluginAuditContext ctx("test_plugin", "");
|
||||
mgr.add_scoped_allowed_root(readonly_dir, /*allow_write=*/false);
|
||||
|
||||
SECTION("a read inside the scoped read-only root is allowed")
|
||||
{
|
||||
CHECK(mgr.check_open((readonly_dir / "vendor.json").string(), "r").allowed);
|
||||
}
|
||||
|
||||
SECTION("a write inside the scoped read-only root is blocked")
|
||||
{
|
||||
CHECK_FALSE(mgr.check_open((readonly_dir / "vendor.json").string(), "w").allowed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,22 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins
|
||||
REQUIRE(read_install_state(plugin_dir, state));
|
||||
CHECK(state.installed_version == "1.2.0");
|
||||
|
||||
state.permissions.fs_read = {"/path/to/read"};
|
||||
state.permissions.fs_readwrite = {"/path/to/readwrite"};
|
||||
state.permissions.network_http = {"https://api.example.com"};
|
||||
state.permissions.network_socket = {"192.168.45.6:443"};
|
||||
state.permissions.process = {"/usr/bin/curl"};
|
||||
REQUIRE(write_install_state(plugin_dir, state));
|
||||
|
||||
// Permission data is persisted in the same sidecar as the installation metadata.
|
||||
PluginInstallState persisted;
|
||||
REQUIRE(read_install_state(plugin_dir, persisted));
|
||||
CHECK(persisted.permissions.fs_read == state.permissions.fs_read);
|
||||
CHECK(persisted.permissions.fs_readwrite == state.permissions.fs_readwrite);
|
||||
CHECK(persisted.permissions.network_http == state.permissions.network_http);
|
||||
CHECK(persisted.permissions.network_socket == state.permissions.network_socket);
|
||||
CHECK(persisted.permissions.process == state.permissions.process);
|
||||
|
||||
// Reading the sidecar back onto a freshly-scanned descriptor (whose header version is still
|
||||
// 1.0.0) must surface the cloud-installed 1.2.0. This is what lets update_cloud_metadata compare
|
||||
// the cloud's latest version against the installed version instead of the stale header, so an
|
||||
@@ -120,4 +136,4 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins
|
||||
scanned.version = "1.0.0"; // as parsed from the unchanged PEP723 header
|
||||
read_install_state(plugin_dir, scanned);
|
||||
CHECK(scanned.installed_version == "1.2.0");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <libslic3r/TriangleMesh.hpp>
|
||||
#include <libslic3r/Format/OBJ.hpp>
|
||||
#include <libslic3r/SVG.hpp>
|
||||
#include <libslic3r/Utils.hpp>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
@@ -33,7 +32,7 @@ inline Slic3r::TriangleMesh load_model(const std::string &obj_filename)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Owns a unique path under the system temp dir, "<prefix>-<unique>[<extension>]"
|
||||
// (parallel-safe, cross-platform). Shared base for the RAII temp guards below.
|
||||
// (parallel-safe, cross-platform). Shared base for the two RAII temp guards below.
|
||||
class ScopedTemporaryPath
|
||||
{
|
||||
public:
|
||||
@@ -71,24 +70,6 @@ public:
|
||||
~ScopedTemporaryDir() { boost::system::error_code ec; boost::filesystem::remove_all(m_path, ec); }
|
||||
};
|
||||
|
||||
// A temp directory that is also Slic3r::temporary_dir() for its lifetime. No test
|
||||
// process sets that global, so code under test which writes there (for example
|
||||
// StepPreProcessor::preprocess) lands at the filesystem root. Restored on scope exit
|
||||
// even when an assertion throws, so it cannot leak into later tests.
|
||||
class ScopedSlic3rTemporaryDir : public ScopedTemporaryDir
|
||||
{
|
||||
public:
|
||||
explicit ScopedSlic3rTemporaryDir(const std::string &prefix = "orca")
|
||||
: ScopedTemporaryDir(prefix), m_previous(Slic3r::temporary_dir())
|
||||
{ Slic3r::set_temporary_dir(string()); }
|
||||
// Runs before ~ScopedTemporaryDir, so the setting goes back while the directory
|
||||
// it names still exists.
|
||||
~ScopedSlic3rTemporaryDir() { Slic3r::set_temporary_dir(m_previous); }
|
||||
|
||||
private:
|
||||
const std::string m_previous;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Debug-only test artifacts
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user