diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c91a748d..f6c6706ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ The release run heads these entries with the version and opens a fresh ## Unreleased +- PDF calculator functions distinguish booleans, integers and reals; correct + bit shifts and rounding; and reject invalid arithmetic and operand types. + - PDF functions reject non-finite values and invalid exponential domains, and interpolate large finite intervals without intermediate overflow. diff --git a/src/odr/internal/pdf/AGENTS.md b/src/odr/internal/pdf/AGENTS.md index 7b29adf6e..6651ed0ff 100644 --- a/src/odr/internal/pdf/AGENTS.md +++ b/src/odr/internal/pdf/AGENTS.md @@ -158,6 +158,8 @@ above. So each run is raised by one font ascent: them one `ClipPath`. The HTML writer draws it as `` in the glyph layer's `@font-face`. Every glyph has its own `x`, so SVG white-space handling moves nothing. +- Calculator operands retain boolean, integer or real types. Integers use + signed 32-bit semantics, with arithmetic overflow promoted to real values. - Calculator functions cap evaluation at 100,000 tokens, 4,096 stack entries and 64 nested calls. Invalid operands or results use the existing zero-result fallback, before range clipping. ISO 32000-1 7.10.5 requires at least 100 diff --git a/src/odr/internal/pdf/pdf_function.cpp b/src/odr/internal/pdf/pdf_function.cpp index 29803b666..5f84abba7 100644 --- a/src/odr/internal/pdf/pdf_function.cpp +++ b/src/odr/internal/pdf/pdf_function.cpp @@ -219,25 +219,17 @@ class SampledFunction final : public Function { // --- type 4: PostScript calculator (ISO 32000-1 7.10.5) -------------------- -/// The integer operand of `idiv`/`mod`/the bitwise operators. Every type-4 -/// value is held as a `double`, and converting one outside the destination -/// range is undefined behaviour, so saturate to the 32-bit signed range a -/// PostScript integer occupies and map NaN to zero. -std::int32_t to_int32(const double v) { - constexpr auto lowest = - static_cast(std::numeric_limits::min()); - constexpr auto highest = - static_cast(std::numeric_limits::max()); - if (std::isnan(v)) { - return 0; - } - return static_cast(std::clamp(v, lowest, highest)); -} +struct CalculatorNumber { + double value{}; + bool integer{}; +}; -/// Whether `x / y` (and `x % y`) is undefined in C++: a zero divisor, or the -/// non-representable `INT32_MIN / -1`. -bool is_undefined_division(const std::int32_t x, const std::int32_t y) { - return y == 0 || (y == -1 && x == std::numeric_limits::min()); +CalculatorNumber calculator_number(const double value, const bool integer) { + if (!std::isfinite(value)) { + throw std::runtime_error("non-finite calculator number"); + } + return {value, integer && value >= std::numeric_limits::min() && + value <= std::numeric_limits::max()}; } /// One token of a type-4 program: a literal number, an operator name, or a @@ -245,7 +237,7 @@ bool is_undefined_division(const std::int32_t x, const std::int32_t y) { struct PostScriptItem { enum class Kind { number, op, block }; Kind kind{Kind::op}; - double number{0}; + CalculatorNumber number; std::string op; std::vector block; }; @@ -262,7 +254,7 @@ class PostScriptFunction final : public Function { std::vector stack; stack.reserve(in.size()); for (const double x : in) { - stack.emplace_back(x); + stack.emplace_back(CalculatorNumber{x, false}); } try { std::size_t remaining = 100000; @@ -274,10 +266,7 @@ class PostScriptFunction final : public Function { } std::vector out(n); for (std::size_t j = n; j-- > 0;) { - out[j] = pop_number(stack); - if (!std::isfinite(out[j])) { - throw std::runtime_error("non-finite calculator result"); - } + out[j] = pop_number(stack).value; } return out; } catch (const std::exception &) { @@ -286,34 +275,36 @@ class PostScriptFunction final : public Function { } private: - using Item = std::variant *>; + using Item = + std::variant *>; - static double pop_number(std::vector &s) { + static Item pop(std::vector &s) { if (s.empty()) { throw std::runtime_error("stack underflow"); } - const double v = std::get(s.back()); + const Item value = s.back(); s.pop_back(); - return v; + return value; + } + + static CalculatorNumber pop_number(std::vector &s) { + return std::get(pop(s)); + } + + static bool pop_boolean(std::vector &s) { + return std::get(pop(s)); } static const std::vector *pop_block(std::vector &s) { - if (s.empty()) { - throw std::runtime_error("stack underflow"); - } - const auto *b = std::get *>(s.back()); - s.pop_back(); - return b; + return std::get *>(pop(s)); } static std::int32_t pop_integer(std::vector &s) { - const double value = pop_number(s); - if (!std::isfinite(value) || std::trunc(value) != value || - value < std::numeric_limits::min() || - value > std::numeric_limits::max()) { + const CalculatorNumber value = pop_number(s); + if (!value.integer) { throw std::runtime_error("invalid calculator integer"); } - return static_cast(value); + return static_cast(value.value); } static std::size_t pop_count(std::vector &s) { @@ -357,86 +348,108 @@ class PostScriptFunction final : public Function { static void run_op(const std::string &op, std::vector &s, std::size_t &remaining, const std::size_t depth) { - const auto unary = [&](double (*f)(double)) { - s.emplace_back(f(pop_number(s))); + const auto unary = [&](double (*f)(double), const bool preserve = false) { + const CalculatorNumber a = pop_number(s); + s.emplace_back(calculator_number(f(a.value), preserve && a.integer)); }; - const auto binary = [&](double (*f)(double, double)) { - const double b = pop_number(s); - const double a = pop_number(s); - s.emplace_back(f(a, b)); + const auto binary = [&](double (*f)(double, double), + const bool preserve = false) { + const CalculatorNumber b = pop_number(s); + const CalculatorNumber a = pop_number(s); + s.emplace_back(calculator_number(f(a.value, b.value), + preserve && a.integer && b.integer)); }; if (op == "add") { - binary([](double a, double b) { return a + b; }); + binary([](double a, double b) { return a + b; }, true); } else if (op == "sub") { - binary([](double a, double b) { return a - b; }); + binary([](double a, double b) { return a - b; }, true); } else if (op == "mul") { - binary([](double a, double b) { return a * b; }); + binary([](double a, double b) { return a * b; }, true); } else if (op == "div") { - binary([](double a, double b) { return b == 0 ? 0.0 : a / b; }); - } else if (op == "idiv") { - binary([](const double a, const double b) { - const std::int32_t x = to_int32(a); - const std::int32_t y = to_int32(b); - // A zero divisor already yielded 0 before; INT32_MIN / -1 joins it. - if (is_undefined_division(x, y)) { - return 0.0; + binary([](double a, double b) { + if (b == 0) { + throw std::runtime_error("calculator division by zero"); } - // NOLINTNEXTLINE(bugprone-integer-division): idiv is integer division - return static_cast(x / y); - }); - } else if (op == "mod") { - binary([](const double a, const double b) { - const std::int32_t x = to_int32(a); - const std::int32_t y = to_int32(b); - return is_undefined_division(x, y) ? 0.0 : static_cast(x % y); + return a / b; }); + } else if (op == "idiv" || op == "mod") { + const std::int32_t b = pop_integer(s); + const std::int32_t a = pop_integer(s); + if (b == 0 || (op == "idiv" && b == -1 && + a == std::numeric_limits::min())) { + throw std::runtime_error("undefined calculator integer division"); + } + const std::int32_t result = op == "idiv" ? a / b : b == -1 ? 0 : a % b; + s.emplace_back(CalculatorNumber{static_cast(result), true}); } else if (op == "neg") { - unary([](double a) { return -a; }); + unary([](double a) { return -a; }, true); } else if (op == "abs") { - unary([](double a) { return std::abs(a); }); + unary([](double a) { return std::abs(a); }, true); } else if (op == "sqrt") { - unary([](double a) { return std::sqrt(std::max(0.0, a)); }); + unary([](double a) { return std::sqrt(a); }); } else if (op == "sin") { unary([](double a) { return std::sin(a / deg); }); } else if (op == "cos") { unary([](double a) { return std::cos(a / deg); }); } else if (op == "atan") { - const double den = pop_number(s); - const double num = pop_number(s); + const double den = pop_number(s).value; + const double num = pop_number(s).value; + if (num == 0 && den == 0) { + throw std::runtime_error("undefined calculator angle"); + } double a = std::atan2(num, den) * deg; if (a < 0) { a += 360; } - s.emplace_back(a); + s.emplace_back(calculator_number(a, false)); } else if (op == "exp") { binary([](double a, double b) { return std::pow(a, b); }); } else if (op == "ln") { - unary([](double a) { return std::log(std::max(1e-12, a)); }); + unary([](double a) { return std::log(a); }); } else if (op == "log") { - unary([](double a) { return std::log10(std::max(1e-12, a)); }); - } else if (op == "cvi" || op == "truncate") { - unary([](double a) { return std::trunc(a); }); + unary([](double a) { return std::log10(a); }); + } else if (op == "cvi") { + const CalculatorNumber value = + calculator_number(std::trunc(pop_number(s).value), true); + if (!value.integer) { + throw std::runtime_error("calculator integer overflow"); + } + s.emplace_back(value); + } else if (op == "truncate") { + unary([](double a) { return std::trunc(a); }, true); } else if (op == "cvr") { - // no-op: every value is already real + s.emplace_back(CalculatorNumber{pop_number(s).value, false}); } else if (op == "floor") { - unary([](double a) { return std::floor(a); }); + unary([](double a) { return std::floor(a); }, true); } else if (op == "ceiling") { - unary([](double a) { return std::ceil(a); }); + unary([](double a) { return std::ceil(a); }, true); } else if (op == "round") { - unary([](double a) { return std::round(a); }); - } else if (op == "eq") { - binary([](double a, double b) { return a == b ? 1.0 : 0.0; }); - } else if (op == "ne") { - binary([](double a, double b) { return a != b ? 1.0 : 0.0; }); - } else if (op == "gt") { - binary([](double a, double b) { return a > b ? 1.0 : 0.0; }); - } else if (op == "ge") { - binary([](double a, double b) { return a >= b ? 1.0 : 0.0; }); - } else if (op == "lt") { - binary([](double a, double b) { return a < b ? 1.0 : 0.0; }); - } else if (op == "le") { - binary([](double a, double b) { return a <= b ? 1.0 : 0.0; }); + unary( + [](double a) { + const double lower = std::floor(a); + return a - lower < 0.5 ? lower : lower + 1; + }, + true); + } else if (op == "eq" || op == "ne") { + const Item b = pop(s); + const Item a = pop(s); + bool equal = false; + if (const auto *number = std::get_if(&a)) { + const auto *other = std::get_if(&b); + equal = other != nullptr && number->value == other->value; + } else if (const auto *boolean = std::get_if(&a)) { + const auto *other = std::get_if(&b); + equal = other != nullptr && *boolean == *other; + } + s.emplace_back(op == "eq" ? equal : !equal); + } else if (op == "gt" || op == "ge" || op == "lt" || op == "le") { + const double b = pop_number(s).value; + const double a = pop_number(s).value; + s.emplace_back(op == "gt" ? a > b + : op == "ge" ? a >= b + : op == "lt" ? a < b + : a <= b); } else if (op == "and") { bitwise_or_logical( s, [](std::int32_t a, std::int32_t b) { return a & b; }, @@ -450,28 +463,26 @@ class PostScriptFunction final : public Function { s, [](std::int32_t a, std::int32_t b) { return a ^ b; }, [](bool a, bool b) { return a != b; }); } else if (op == "not") { - const double a = pop_number(s); - if (a == 0.0 || a == 1.0) { - s.emplace_back(a == 0.0 ? 1.0 : 0.0); + if (!s.empty() && std::holds_alternative(s.back())) { + s.emplace_back(!pop_boolean(s)); } else { - s.emplace_back(static_cast(~to_int32(a))); + s.emplace_back( + CalculatorNumber{static_cast(~pop_integer(s)), true}); } } else if (op == "bitshift") { - const double shift = pop_number(s); - const std::int32_t value = to_int32(pop_number(s)); - // Shifting a 32-bit value by 32 or more is undefined in C++; PostScript - // shifts every bit out. (The comparison also catches a NaN shift.) - const double magnitude = std::abs(shift); - const std::int32_t by = - magnitude < 32.0 ? static_cast(magnitude) : 32; - s.emplace_back(static_cast( - by == 32 ? 0 : (shift >= 0 ? value << by : value >> by))); + const std::int32_t shift = pop_integer(s); + const auto value = static_cast(pop_integer(s)); + const std::uint32_t result = shift <= -32 || shift >= 32 ? 0 + : shift >= 0 ? value << shift + : value >> -shift; + s.emplace_back(CalculatorNumber{ + static_cast(static_cast(result)), true}); } else if (op == "true") { - s.emplace_back(1.0); + s.emplace_back(true); } else if (op == "false") { - s.emplace_back(0.0); + s.emplace_back(false); } else if (op == "pop") { - pop_number(s); + pop(s); } else if (op == "exch") { const Item b = s.at(s.size() - 1); const Item a = s.at(s.size() - 2); @@ -503,15 +514,15 @@ class PostScriptFunction final : public Function { } } else if (op == "if") { const auto *proc = pop_block(s); - const double cond = pop_number(s); - if (cond != 0.0) { + const bool cond = pop_boolean(s); + if (cond) { run(*proc, s, remaining, depth + 1); } } else if (op == "ifelse") { const auto *proc2 = pop_block(s); const auto *proc1 = pop_block(s); - const double cond = pop_number(s); - run(cond != 0.0 ? *proc1 : *proc2, s, remaining, depth + 1); + const bool cond = pop_boolean(s); + run(cond ? *proc1 : *proc2, s, remaining, depth + 1); } else { throw std::runtime_error("unknown PostScript operator: " + op); } @@ -520,13 +531,14 @@ class PostScriptFunction final : public Function { template static void bitwise_or_logical(std::vector &s, IntOp int_op, BoolOp bool_op) { - const double b = pop_number(s); - const double a = pop_number(s); - const bool boolean = (a == 0.0 || a == 1.0) && (b == 0.0 || b == 1.0); - if (boolean) { - s.emplace_back(bool_op(a != 0.0, b != 0.0) ? 1.0 : 0.0); + if (!s.empty() && std::holds_alternative(s.back())) { + const bool b = pop_boolean(s); + const bool a = pop_boolean(s); + s.emplace_back(bool_op(a, b)); } else { - s.emplace_back(static_cast(int_op(to_int32(a), to_int32(b)))); + const std::int32_t b = pop_integer(s); + const std::int32_t a = pop_integer(s); + s.emplace_back(CalculatorNumber{static_cast(int_op(a, b)), true}); } } @@ -579,7 +591,8 @@ parse_postscript(const std::string &text, std::size_t &pos, return std::nullopt; } item.kind = PostScriptItem::Kind::number; - item.number = *number; + item.number = calculator_number(*number, token.find_first_of(".eE") == + std::string_view::npos); } else { item.kind = PostScriptItem::Kind::op; item.op = token; diff --git a/test/src/internal/pdf/pdf_function.cpp b/test/src/internal/pdf/pdf_function.cpp index 63d5356a4..7522b214b 100644 --- a/test/src/internal/pdf/pdf_function.cpp +++ b/test/src/internal/pdf/pdf_function.cpp @@ -304,10 +304,37 @@ TEST(PdfFunction, calculator_invalid_execution_returns_zero) { dict["Domain"] = reals({0, 1}); dict["Range"] = reals({0, 1}); for (const std::string program : - {"{ pop {} }", "{ pop }", "{ pop 1e308 dup mul }", "{ -1 copy }", - "{ 1e100 copy }", "{ 0.5 copy }", "{ 2 copy }", "{ -1 index }", - "{ 1e100 index }", "{ 1 index }", "{ -1 0 roll }", "{ 1 1e100 roll }", - "{ 1 0.5 roll }", "{ pop { dup true exch if } dup true exch if }"}) { + {"{ pop {} }", + "{ pop }", + "{ pop 1e308 dup mul }", + "{ -1 copy }", + "{ 1e100 copy }", + "{ 0.5 copy }", + "{ 2 copy }", + "{ -1 index }", + "{ 1e100 index }", + "{ 1 index }", + "{ -1 0 roll }", + "{ 1 1e100 roll }", + "{ 1 0.5 roll }", + "{ pop true }", + "{ pop true 1 add }", + "{ pop 1 { 1 } if }", + "{ pop 1.0 not }", + "{ pop true 1 and }", + "{ pop 1 0 div 1 add }", + "{ pop -1 sqrt 1 add }", + "{ pop 0 ln pop 1 }", + "{ pop 0 log pop 1 }", + "{ pop 1.5 cvi 1.0 idiv }", + "{ pop 1.0 1 bitshift }", + "{ pop 1 0.5 bitshift }", + "{ pop 2147483648 cvi pop 1 }", + "{ pop 1 cvr not }", + "{ pop 1.0 1 add 1 and }", + "{ pop 1.0 1 mod 1 add }", + "{ pop -2147483648 -1 idiv 1 add }", + "{ pop { dup true exch if } dup true exch if }"}) { SCOPED_TRACE(program); const auto fn = parse_function(Object(dict), context(program)); ASSERT_NE(fn, nullptr); @@ -395,3 +422,28 @@ TEST(PdfFunction, rejects_non_finite_values) { EXPECT_DOUBLE_EQ(clipped->eval({2})[0], 1 - start); } } + +TEST(PdfFunction, calculator_operand_types_and_numeric_operators) { + Dictionary dict; + dict["FunctionType"] = Object(Integer{4}); + dict["Domain"] = reals({0, 1}); + dict["Range"] = reals({-2147483648.0, 2147483647.0}); + for (const auto &[program, expected] : std::array{ + std::pair{"0 not", -1.0}, std::pair{"1 not", -2.0}, + std::pair{"-1 -1 bitshift", 2147483647.0}, + std::pair{"-6.5 round", -6.0}, + std::pair{"true not { 3 } { 4 } ifelse", 4.0}, + std::pair{"true false xor { 3 } { 4 } ifelse", 3.0}, + std::pair{"true false and pop 3", 3.0}, + std::pair{"true 1 eq { 3 } { 4 } ifelse", 4.0}, + std::pair{"1 1.0 eq { 3 } { 4 } ifelse", 3.0}, + std::pair{"2 3 add 1 and", 1.0}, + std::pair{"-2147483648 neg 2147483648 eq { 3 } { 4 } ifelse", 3.0}, + std::pair{"3.9 cvi 2 idiv", 1.0}}) { + SCOPED_TRACE(program); + const auto fn = parse_function( + Object(dict), context(std::string("{ pop ") + program + " }")); + ASSERT_NE(fn, nullptr); + EXPECT_DOUBLE_EQ(fn->eval({0.5})[0], expected); + } +}