From 94c0da11d33f5f987f425f3d46229bd181e036da Mon Sep 17 00:00:00 2001 From: verstach Date: Tue, 15 Sep 2026 02:14:13 -0700 Subject: [PATCH] fix(runtime): saturate overflowing numeric string casts --- src/runtime/value.zig | 26 ++++++++++++++++++++++++- tests/numeric_string_int_saturation.php | 24 +++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/numeric_string_int_saturation.php diff --git a/src/runtime/value.zig b/src/runtime/value.zig index b0e6d3a7..599beb1c 100644 --- a/src/runtime/value.zig +++ b/src/runtime/value.zig @@ -1570,7 +1570,8 @@ pub const Value = union(enum) { const f = parseLeadingFloat(s[start..]); if (!std.math.isFinite(f)) return 0; const max_f: f64 = 9.2233720368547758e18; - if (f >= max_f or f < -max_f) return 0; + if (f >= max_f) return std.math.maxInt(i64); + if (f <= -max_f) return std.math.minInt(i64); return @intFromFloat(f); } // saturating parse: matches PHP's "(int)" which clamps @@ -1980,6 +1981,29 @@ test "numeric string exponent requires digits" { } } +test "parse leading int float saturation" { + // positive overflow + try std.testing.expectEqual(std.math.maxInt(i64), Value.toInt(.{ .string = Value.String.borrowed("1e100") })); + try std.testing.expectEqual(std.math.maxInt(i64), Value.toInt(.{ .string = Value.String.borrowed("1e19") })); + try std.testing.expectEqual(std.math.maxInt(i64), Value.toInt(.{ .string = Value.String.borrowed("9.9e18") })); + try std.testing.expectEqual(std.math.maxInt(i64), Value.toInt(.{ .string = Value.String.borrowed("9.223372036854776e18") })); + try std.testing.expectEqual(std.math.maxInt(i64), Value.toInt(.{ .string = Value.String.borrowed("9223372036854775807.0") })); + + // negative overflow + try std.testing.expectEqual(std.math.minInt(i64), Value.toInt(.{ .string = Value.String.borrowed("-1e100") })); + try std.testing.expectEqual(std.math.minInt(i64), Value.toInt(.{ .string = Value.String.borrowed("-1e19") })); + try std.testing.expectEqual(std.math.minInt(i64), Value.toInt(.{ .string = Value.String.borrowed("-9.9e18") })); + try std.testing.expectEqual(std.math.minInt(i64), Value.toInt(.{ .string = Value.String.borrowed("-9.223372036854776e18") })); + try std.testing.expectEqual(std.math.minInt(i64), Value.toInt(.{ .string = Value.String.borrowed("-9223372036854775808.0") })); + + // boundary and in-range cases + try std.testing.expectEqual(@as(i64, 9223372036854774784), Value.toInt(.{ .string = Value.String.borrowed("9.223372036854775e18") })); + try std.testing.expectEqual(@as(i64, -9223372036854774784), Value.toInt(.{ .string = Value.String.borrowed("-9.223372036854775e18") })); + try std.testing.expectEqual(@as(i64, 100000), Value.toInt(.{ .string = Value.String.borrowed("1e5") })); + try std.testing.expectEqual(@as(i64, 123), Value.toInt(.{ .string = Value.String.borrowed("123.456") })); + try std.testing.expectEqual(@as(i64, -123), Value.toInt(.{ .string = Value.String.borrowed("-123.456") })); +} + test "truthiness" { try std.testing.expect(!Value.isTruthy(.null)); try std.testing.expect(!Value.isTruthy(.{ .bool = false })); diff --git a/tests/numeric_string_int_saturation.php b/tests/numeric_string_int_saturation.php new file mode 100644 index 00000000..87d46446 --- /dev/null +++ b/tests/numeric_string_int_saturation.php @@ -0,0 +1,24 @@ +