mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-30 12:21:05 +00:00
Fix CLI Crashes on Malformed Project, Assemble List and No-Input Runs
Four CLI paths indexed vectors without checking their size and crashed with SIGSEGV on malformed input: - A project inherits_group whose length is not the filament count plus the process and printer entries was split by position. It is now ignored with a warning, as if the project had none. - An assemble list object with an empty filaments list passed validation and was then read at index 0. It is now rejected as a config error, as is a negative filament id. - --slice N --arrange 1 on a project without plate metadata read the missing plate data. It now falls back to the plate's own filaments, like the other plate data reads. - --assemble with no input model built an object with no volumes. It is now rejected as invalid parameters. A tests/cli script covers each case through the binary, since all four live inline in CLI::run().
This commit is contained in:
+24
-2
@@ -699,11 +699,22 @@ static int load_assemble_plate_list(std::string config_file, std::vector<assembl
|
|||||||
}
|
}
|
||||||
|
|
||||||
assemble_object.filaments = object_json.at(JSON_ASSEMPLE_OBJECT_FILAMENTS).get<std::vector<int>>();
|
assemble_object.filaments = object_json.at(JSON_ASSEMPLE_OBJECT_FILAMENTS).get<std::vector<int>>();
|
||||||
if ((assemble_object.filaments.size() > 0) && (assemble_object.filaments.size() != assemble_object.count) && (assemble_object.filaments.size() != 1))
|
if (assemble_object.filaments.empty())
|
||||||
|
{
|
||||||
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s filaments list is empty") % assemble_object.path;
|
||||||
|
return CLI_CONFIG_FILE_ERROR;
|
||||||
|
}
|
||||||
|
if ((assemble_object.filaments.size() != assemble_object.count) && (assemble_object.filaments.size() != 1))
|
||||||
{
|
{
|
||||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s filaments count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.filaments.size() % assemble_object.count;
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s filaments count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.filaments.size() % assemble_object.count;
|
||||||
return CLI_CONFIG_FILE_ERROR;
|
return CLI_CONFIG_FILE_ERROR;
|
||||||
}
|
}
|
||||||
|
// 0 keeps the default filament, as it does for --load-filament-ids.
|
||||||
|
if (std::any_of(assemble_object.filaments.begin(), assemble_object.filaments.end(), [](int id) { return id < 0; }))
|
||||||
|
{
|
||||||
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1% has a negative filament id") % assemble_object.path;
|
||||||
|
return CLI_CONFIG_FILE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
if (object_json.contains(JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX)) {
|
if (object_json.contains(JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX)) {
|
||||||
assemble_object.assemble_index = object_json[JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX].get<std::vector<int>>();
|
assemble_object.assemble_index = object_json[JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX].get<std::vector<int>>();
|
||||||
@@ -1863,6 +1874,12 @@ int CLI::run(int argc, char **argv)
|
|||||||
|
|
||||||
BOOST_LOG_TRIVIAL(info) << boost::format("current_printer_name %1%, current_process_name %2%")%current_printer_name %current_process_name;
|
BOOST_LOG_TRIVIAL(info) << boost::format("current_printer_name %1%, current_process_name %2%")%current_printer_name %current_process_name;
|
||||||
ConfigOptionStrings* option_strings = config.option<ConfigOptionStrings>("inherits_group");
|
ConfigOptionStrings* option_strings = config.option<ConfigOptionStrings>("inherits_group");
|
||||||
|
// One entry for the process, one per filament and one for the printer.
|
||||||
|
if (option_strings && option_strings->values.size() != current_filaments_name.size() + 2) {
|
||||||
|
boost::nowide::cerr << "Warning: ignoring inherits_group with " << option_strings->values.size() << " entries, expected "
|
||||||
|
<< current_filaments_name.size() + 2 << " for " << current_filaments_name.size() << " filaments" << std::endl;
|
||||||
|
option_strings = nullptr;
|
||||||
|
}
|
||||||
if (option_strings) {
|
if (option_strings) {
|
||||||
current_inherits_group = option_strings->values;
|
current_inherits_group = option_strings->values;
|
||||||
size_t size = current_inherits_group.size();
|
size_t size = current_inherits_group.size();
|
||||||
@@ -4733,6 +4750,11 @@ int CLI::run(int argc, char **argv)
|
|||||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||||
flush_and_exit(CLI_INVALID_PARAMS);
|
flush_and_exit(CLI_INVALID_PARAMS);
|
||||||
}
|
}
|
||||||
|
if (m_models.empty()) {
|
||||||
|
boost::nowide::cerr << "Invalid params: --assemble needs at least one input model." << std::endl;
|
||||||
|
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||||
|
flush_and_exit(CLI_INVALID_PARAMS);
|
||||||
|
}
|
||||||
Model m;
|
Model m;
|
||||||
ModelObject* new_object = m.add_object();
|
ModelObject* new_object = m.add_object();
|
||||||
new_object->name = _u8L("Assembly");
|
new_object->name = _u8L("Assembly");
|
||||||
@@ -5673,7 +5695,7 @@ int CLI::run(int argc, char **argv)
|
|||||||
float w = dynamic_cast<const ConfigOptionFloat *>(m_print_config.option("prime_tower_width"))->value;
|
float w = dynamic_cast<const ConfigOptionFloat *>(m_print_config.option("prime_tower_width"))->value;
|
||||||
float a = dynamic_cast<const ConfigOptionFloat *>(m_print_config.option("wipe_tower_rotation_angle"))->value;
|
float a = dynamic_cast<const ConfigOptionFloat *>(m_print_config.option("wipe_tower_rotation_angle"))->value;
|
||||||
float v = dynamic_cast<const ConfigOptionFloat *>(m_print_config.option("prime_volume"))->value;
|
float v = dynamic_cast<const ConfigOptionFloat *>(m_print_config.option("prime_volume"))->value;
|
||||||
unsigned int filaments_cnt = plate_data_src[plate_to_slice-1]->slice_filaments_info.size();
|
unsigned int filaments_cnt = (plate_data_src.size() >= static_cast<size_t>(plate_to_slice)) ? plate_data_src[plate_to_slice-1]->slice_filaments_info.size() : 0;
|
||||||
if ((filaments_cnt == 0) || need_skip)
|
if ((filaments_cnt == 0) || need_skip)
|
||||||
{
|
{
|
||||||
// slice filaments info invalid
|
// slice filaments info invalid
|
||||||
|
|||||||
@@ -15,3 +15,10 @@ set_tests_properties(cli_strict_mode PROPERTIES
|
|||||||
LABELS "CLI;RequiresApp"
|
LABELS "CLI;RequiresApp"
|
||||||
SKIP_RETURN_CODE 77
|
SKIP_RETURN_CODE 77
|
||||||
TIMEOUT 900)
|
TIMEOUT 900)
|
||||||
|
|
||||||
|
add_test(NAME cli_malformed_input
|
||||||
|
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_malformed_input.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON})
|
||||||
|
set_tests_properties(cli_malformed_input PROPERTIES
|
||||||
|
LABELS "CLI;RequiresApp"
|
||||||
|
SKIP_RETURN_CODE 77
|
||||||
|
TIMEOUT 900)
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# End-to-end checks that malformed CLI input fails cleanly, or loads, instead of crashing the
|
||||||
|
# orca-slicer binary. Each case lives inline in CLI::run(), so only the binary can reach it.
|
||||||
|
#
|
||||||
|
# - A project whose inherits_group does not have one entry per filament plus the process and
|
||||||
|
# printer entries is loaded as if it had none.
|
||||||
|
# - --slice N --arrange 1 on a project without plate metadata slices plate N.
|
||||||
|
# - An assemble list object with an empty filament list, or a negative filament id, is rejected
|
||||||
|
# as a config error.
|
||||||
|
# - --assemble with no input model is rejected as invalid parameters.
|
||||||
|
#
|
||||||
|
# usage: test_cli_malformed_input.sh <orca-slicer binary> <python3>
|
||||||
|
set -u
|
||||||
|
|
||||||
|
BIN="${1:-}"
|
||||||
|
PY="${2:-python3}"
|
||||||
|
# 77 is the test's SKIP_RETURN_CODE.
|
||||||
|
[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; }
|
||||||
|
|
||||||
|
# From src/libslic3r/Utils.hpp. main() returns them, so the shell sees them modulo 256.
|
||||||
|
CLI_SUCCESS=0
|
||||||
|
CLI_INVALID_PARAMS=-2
|
||||||
|
CLI_CONFIG_FILE_ERROR=-5
|
||||||
|
|
||||||
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-malformed.XXXXXX")"
|
||||||
|
trap 'rm -rf "$WORK"' EXIT
|
||||||
|
mkdir -p "$WORK/datadir"
|
||||||
|
|
||||||
|
# Standalone presets: without "inherits" the CLI loads them as-is, with no preset bundle.
|
||||||
|
cat > "$WORK/machine.json" <<'EOF'
|
||||||
|
{
|
||||||
|
"type": "machine",
|
||||||
|
"from": "User",
|
||||||
|
"name": "CLI malformed input test printer",
|
||||||
|
"printable_area": ["0x0", "200x0", "200x200", "0x200"],
|
||||||
|
"printable_height": "100",
|
||||||
|
"layer_change_gcode": "G92 E0"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
cat > "$WORK/process.json" <<'EOF'
|
||||||
|
{
|
||||||
|
"type": "process",
|
||||||
|
"from": "User",
|
||||||
|
"name": "CLI malformed input test process"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
cat > "$WORK/filament.json" <<'EOF'
|
||||||
|
{
|
||||||
|
"type": "filament",
|
||||||
|
"from": "User",
|
||||||
|
"name": "CLI malformed input test filament"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
"$PY" - "$WORK/cube.stl" <<'EOF'
|
||||||
|
import sys
|
||||||
|
|
||||||
|
v = [(x, y, z) for z in (0, 10) for y in (0, 10) for x in (0, 10)]
|
||||||
|
with open(sys.argv[1], "w") as f:
|
||||||
|
f.write("solid cube\n")
|
||||||
|
# Faces wound counter-clockwise seen from outside: -z, +z, -y, +y, -x, +x.
|
||||||
|
for a, b, c, d in ((0, 2, 3, 1), (4, 5, 7, 6), (0, 1, 5, 4), (2, 6, 7, 3), (0, 4, 6, 2), (1, 3, 7, 5)):
|
||||||
|
for tri in ((a, b, c), (a, c, d)):
|
||||||
|
f.write("facet normal 0 0 0\nouter loop\n")
|
||||||
|
for i in tri:
|
||||||
|
f.write("vertex %g %g %g\n" % v[i])
|
||||||
|
f.write("endloop\nendfacet\n")
|
||||||
|
f.write("endsolid cube\n")
|
||||||
|
EOF
|
||||||
|
|
||||||
|
fails=0
|
||||||
|
fail() { echo "FAIL: $*"; fails=$((fails + 1)); }
|
||||||
|
|
||||||
|
# run <tag> [option...]: run into $WORK/<tag>, keeping the log and the shell status there.
|
||||||
|
run() {
|
||||||
|
local out="$WORK/$1"; shift
|
||||||
|
mkdir -p "$out"
|
||||||
|
timeout 300 "$BIN" --datadir "$WORK/datadir" --outputdir "$out" "$@" > "$out/log" 2>&1
|
||||||
|
echo $? > "$out/status"
|
||||||
|
}
|
||||||
|
|
||||||
|
# run_presets <tag> [option...]: run with the standalone presets loaded.
|
||||||
|
run_presets() {
|
||||||
|
local tag="$1"; shift
|
||||||
|
run "$tag" --load-settings "$WORK/machine.json;$WORK/process.json" --load-filaments "$WORK/filament.json" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# expect_status <tag> <cli code>
|
||||||
|
expect_status() {
|
||||||
|
local got; got="$(cat "$WORK/$1/status")"
|
||||||
|
[ "$got" -eq $(( $2 & 255 )) ] || fail "$1: shell status $got, want $(( $2 & 255 )) (code $2)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# expect_result <tag> <return_code>: a failing run must also carry an error_string.
|
||||||
|
expect_result() {
|
||||||
|
"$PY" - "$WORK/$1/result.json" "$2" <<'EOF' || fail "$1: result.json"
|
||||||
|
import json, sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(sys.argv[1]) as f:
|
||||||
|
result = json.load(f)
|
||||||
|
except (OSError, ValueError) as e:
|
||||||
|
sys.exit("cannot read %s: %s" % (sys.argv[1], e))
|
||||||
|
want_rc = int(sys.argv[2])
|
||||||
|
if result.get("return_code") != want_rc:
|
||||||
|
sys.exit("return_code %r, want %d" % (result.get("return_code"), want_rc))
|
||||||
|
if want_rc != 0 and not result.get("error_string"):
|
||||||
|
sys.exit("no error_string")
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# expect_log <tag> <text>
|
||||||
|
expect_log() {
|
||||||
|
grep -qF -- "$2" "$WORK/$1/log" || fail "$1: log does not mention \"$2\""
|
||||||
|
}
|
||||||
|
|
||||||
|
# expect_gcode <tag>
|
||||||
|
expect_gcode() {
|
||||||
|
compgen -G "$WORK/$1/*.gcode" > /dev/null || fail "$1: no G-code was exported"
|
||||||
|
}
|
||||||
|
|
||||||
|
# rewrite_3mf <in> <out> inherits <json list> | no-plates
|
||||||
|
rewrite_3mf() {
|
||||||
|
"$PY" - "$@" <<'EOF'
|
||||||
|
import json, re, sys, zipfile
|
||||||
|
|
||||||
|
src, dst, mode = sys.argv[1:4]
|
||||||
|
with zipfile.ZipFile(src) as zin, zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout:
|
||||||
|
for info in zin.infolist():
|
||||||
|
data = zin.read(info.filename)
|
||||||
|
if mode == "inherits" and info.filename == "Metadata/project_settings.config":
|
||||||
|
config = json.loads(data)
|
||||||
|
config["inherits_group"] = json.loads(sys.argv[4])
|
||||||
|
data = json.dumps(config, indent=4).encode()
|
||||||
|
elif mode == "no-plates":
|
||||||
|
if re.match(r"Metadata/plate_\d+\.", info.filename):
|
||||||
|
continue
|
||||||
|
if info.filename == "Metadata/model_settings.config":
|
||||||
|
data = re.sub(rb"\s*<plate>.*?</plate>", b"", data, flags=re.S)
|
||||||
|
zout.writestr(info, data)
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# assemble_list <file> <filaments json>
|
||||||
|
assemble_list() {
|
||||||
|
cat > "$1" <<EOF
|
||||||
|
{"plates": [{"plate_name": "p", "need_arrange": false,
|
||||||
|
"objects": [{"path": "$WORK/cube.stl", "count": 1, "filaments": $2,
|
||||||
|
"pos_x": [100], "pos_y": [100]}]}]}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "== a one-filament project exported by the CLI is the base for the project cases"
|
||||||
|
run_presets export --slice 0 --export-3mf project.3mf "$WORK/cube.stl"
|
||||||
|
expect_status export $CLI_SUCCESS
|
||||||
|
[ -f "$WORK/export/project.3mf" ] || { echo "FAIL: project export failed"; tail -n 40 "$WORK/export/log"; exit 1; }
|
||||||
|
|
||||||
|
echo "== an inherits_group of the wrong length is ignored"
|
||||||
|
for group in '[]' '[""]' '["", "", "", "", ""]'; do
|
||||||
|
tag="inherits_$("$PY" -c 'import json, sys; print(len(json.loads(sys.argv[1])))' "$group")"
|
||||||
|
rewrite_3mf "$WORK/export/project.3mf" "$WORK/$tag.3mf" inherits "$group"
|
||||||
|
run "$tag" --info "$WORK/$tag.3mf"
|
||||||
|
expect_status "$tag" $CLI_SUCCESS
|
||||||
|
expect_log "$tag" "inherits_group"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "== --slice 1 --arrange 1 slices a project without plate metadata"
|
||||||
|
rewrite_3mf "$WORK/export/project.3mf" "$WORK/no_plates.3mf" no-plates
|
||||||
|
run_presets no_plates --slice 1 --arrange 1 "$WORK/no_plates.3mf"
|
||||||
|
expect_status no_plates $CLI_SUCCESS
|
||||||
|
expect_result no_plates $CLI_SUCCESS
|
||||||
|
expect_gcode no_plates
|
||||||
|
|
||||||
|
echo "== an assemble list with a valid filament id slices"
|
||||||
|
assemble_list "$WORK/assemble_valid.json" '[1]'
|
||||||
|
run_presets assemble_valid --slice 0 --load-assemble-list "$WORK/assemble_valid.json"
|
||||||
|
expect_status assemble_valid $CLI_SUCCESS
|
||||||
|
expect_gcode assemble_valid
|
||||||
|
|
||||||
|
echo "== an assemble list with an empty filament list or a negative filament id is rejected"
|
||||||
|
for filaments in '[]' '[-1]'; do
|
||||||
|
if [ "$filaments" = '[]' ]; then tag=assemble_empty; else tag=assemble_negative; fi
|
||||||
|
assemble_list "$WORK/$tag.json" "$filaments"
|
||||||
|
run_presets "$tag" --slice 0 --load-assemble-list "$WORK/$tag.json"
|
||||||
|
expect_status "$tag" $CLI_CONFIG_FILE_ERROR
|
||||||
|
expect_result "$tag" $CLI_CONFIG_FILE_ERROR
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "== --assemble with no input model is rejected"
|
||||||
|
for action in "--slice 0" "--export-3mf out.3mf"; do
|
||||||
|
tag="assemble_no_input_${action%% *}"
|
||||||
|
tag="${tag//-/}"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
run_presets "$tag" --assemble $action
|
||||||
|
expect_status "$tag" $CLI_INVALID_PARAMS
|
||||||
|
expect_result "$tag" $CLI_INVALID_PARAMS
|
||||||
|
expect_log "$tag" "--assemble"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$fails" -ne 0 ]; then
|
||||||
|
for log in "$WORK"/*/log; do
|
||||||
|
echo "--- $log"
|
||||||
|
tail -n 40 "$log"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "PASS"
|
||||||
Reference in New Issue
Block a user