Check the canvas size before narrowing it to an int
A canvas side was cast to int with no range check, so a number that did not fit came back as one that did: `-s 4294968296x600` rendered a perfectly plausible 1000x600, and a hand-edited project file could ask for a width the reader turned into something else (or, for a double out of range, into undefined behaviour). Both front-ends now check the side before the cast, against one shared bound in chain.hpp — the 1..20000 the GUI spin buttons already offered, so the help text, the CLI error, the project reader and the interface all move together. The tests gain a `canvas size` section covering the round trip at both ends of the range and the refusals on either side of it, through the project reader and through `-s`; the test binary picks up the CLI parser to do it, which stays GUI-free. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
add_executable(effector_tests core_tests.cpp)
|
||||
# The CLI parser joins the core here: it is GUI-free too, and the canvas size
|
||||
# it produces has to obey the same bounds a project file does.
|
||||
add_executable(effector_tests core_tests.cpp ../src/cli/options.cpp)
|
||||
target_link_libraries(effector_tests PRIVATE effector_core)
|
||||
target_compile_options(effector_tests PRIVATE -Wall -Wextra -Wpedantic)
|
||||
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "cli/options.hpp"
|
||||
#include "core/chain.hpp"
|
||||
#include "core/project.hpp"
|
||||
#include "core/registry.hpp"
|
||||
@@ -44,6 +47,28 @@ effector::Image gradient(int width, int height, int shade) {
|
||||
return image;
|
||||
}
|
||||
|
||||
// A canvas taken from a project file, written by hand rather than by
|
||||
// projectToJson, so a size no writer would produce can be fed to the reader.
|
||||
std::string projectWithCanvas(const std::string& width, const std::string& height) {
|
||||
return "{\"canvas\": {\"width\": " + width + ", \"height\": " + height +
|
||||
"},\n \"chain\": [{\"effect\": \"blur\"}]}";
|
||||
}
|
||||
|
||||
// The canvas a `-s` value ends up as, or nothing when the parser refuses it.
|
||||
std::optional<std::pair<int, int>> canvasFromSize(const std::string& size) {
|
||||
char program[] = "effector";
|
||||
char flag[] = "-s";
|
||||
std::vector<char> value(size.begin(), size.end());
|
||||
value.push_back('\0');
|
||||
char* argv[] = {program, flag, value.data()};
|
||||
try {
|
||||
const auto options = effector::parseOptions(3, argv);
|
||||
return std::pair<int, int>{options.width, options.height};
|
||||
} catch (const effector::EffectError&) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
bool sameBytes(const effector::Image& a, const effector::Image& b) {
|
||||
if (a.width() != b.width() || a.height() != b.height()) return false;
|
||||
return std::equal(a.data(), a.data() + a.byteSize(), b.data());
|
||||
@@ -430,6 +455,72 @@ int main() {
|
||||
std::filesystem::remove_all(dir);
|
||||
}
|
||||
|
||||
std::cout << "canvas size\n";
|
||||
{
|
||||
// The sizes the front-ends actually offer make the round trip whole.
|
||||
int kept = 0;
|
||||
for (const auto [w, h] : {std::pair<int, int>{1, 1},
|
||||
{640, 400},
|
||||
{1920, 1080},
|
||||
{kMaxCanvasSide, kMaxCanvasSide}}) {
|
||||
const auto back = projectFromJson(
|
||||
projectWithCanvas(std::to_string(w), std::to_string(h)));
|
||||
if (back.width == w && back.height == h) ++kept;
|
||||
}
|
||||
check(kept == 4, "every canvas in range is read back exactly");
|
||||
|
||||
Project project;
|
||||
project.stages.push_back({effectByName("blur"), {}});
|
||||
project.width = kMaxCanvasSide;
|
||||
project.height = 3;
|
||||
const auto written = projectFromJson(projectToJson(project));
|
||||
check(written.width == kMaxCanvasSide && written.height == 3,
|
||||
"the largest canvas survives a write and a read");
|
||||
|
||||
// A side that does not fit in an int must not be narrowed into one
|
||||
// that does: 4294968296 truncates to a perfectly plausible 1000.
|
||||
int refusals = 0;
|
||||
for (const char* bad : {"4294968296", "2147483648", "99999999999999999999", "-5", "0",
|
||||
"20001", "800.5", "\"800\"", "true"}) {
|
||||
try {
|
||||
projectFromJson(projectWithCanvas(bad, "600"));
|
||||
} catch (const EffectError&) {
|
||||
++refusals;
|
||||
}
|
||||
}
|
||||
check(refusals == 9, "a canvas width an int cannot hold is refused, not wrapped");
|
||||
|
||||
refusals = 0;
|
||||
for (const char* bad : {"4294968296", "-5", "0", "20001", "800.5"}) {
|
||||
try {
|
||||
projectFromJson(projectWithCanvas("800", bad));
|
||||
} catch (const EffectError&) {
|
||||
++refusals;
|
||||
}
|
||||
}
|
||||
check(refusals == 5, "the same bounds apply to the canvas height");
|
||||
|
||||
// A canvas the file does not mention keeps the defaults rather than
|
||||
// failing: an old project is still readable.
|
||||
const auto bare = projectFromJson("{\"chain\": [{\"effect\": \"blur\"}]}");
|
||||
check(bare.width == 1920 && bare.height == 1080, "a project with no canvas keeps 1920x1080");
|
||||
|
||||
// The CLI narrows the same way, from a long rather than a double.
|
||||
const auto plain = canvasFromSize("800x600");
|
||||
check(plain && plain->first == 800 && plain->second == 600, "-s 800x600 is stored as given");
|
||||
const auto big = canvasFromSize(std::to_string(kMaxCanvasSide) + "x" +
|
||||
std::to_string(kMaxCanvasSide));
|
||||
check(big && big->first == kMaxCanvasSide, "-s takes the largest canvas");
|
||||
check(!canvasFromSize("4294968296x600"), "-s refuses a width that does not fit in an int");
|
||||
check(!canvasFromSize("2147483648x600"), "-s refuses a width just past INT_MAX");
|
||||
check(!canvasFromSize("99999999999999999999x600"), "-s refuses a width past a long");
|
||||
check(!canvasFromSize("800x4294968296"), "-s refuses a height that does not fit in an int");
|
||||
check(!canvasFromSize("20001x600") && !canvasFromSize("800x20001"),
|
||||
"-s refuses a side past the canvas limit");
|
||||
check(!canvasFromSize("0x600") && !canvasFromSize("-5x600"),
|
||||
"-s refuses a side that is not positive");
|
||||
}
|
||||
|
||||
std::cout << "image io\n";
|
||||
{
|
||||
const auto path = std::filesystem::temp_directory_path() / "effector_test_roundtrip.png";
|
||||
|
||||
Reference in New Issue
Block a user