From a1741b0d9be7a22b078f9d597419aaa1d4aba673 Mon Sep 17 00:00:00 2001
From: Joseph Koshakow <koshy44@gmail.com>
Date: Thu, 13 Jun 2024 22:39:25 -0400
Subject: [PATCH v11 1/1] Handle overflow in money arithmetic

---
 src/backend/utils/adt/cash.c        | 174 +++++++++++++++-------------
 src/test/regress/expected/money.out | 106 +++++++++++++++++
 src/test/regress/sql/money.sql      |  47 ++++++++
 3 files changed, 247 insertions(+), 80 deletions(-)

diff --git a/src/backend/utils/adt/cash.c b/src/backend/utils/adt/cash.c
index 32fbad2f57..b20c358486 100644
--- a/src/backend/utils/adt/cash.c
+++ b/src/backend/utils/adt/cash.c
@@ -26,6 +26,7 @@
 #include "libpq/pqformat.h"
 #include "utils/builtins.h"
 #include "utils/cash.h"
+#include "utils/float.h"
 #include "utils/numeric.h"
 #include "utils/pg_locale.h"
 
@@ -86,6 +87,82 @@ num_word(Cash value)
 	return buf;
 }								/* num_word() */
 
+static inline Cash
+cash_pl_cash(Cash c1, Cash c2)
+{
+	Cash		res;
+
+	if (unlikely(pg_add_s64_overflow(c1, c2, &res)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
+
+	return res;
+}
+
+static inline Cash
+cash_mi_cash(Cash c1, Cash c2)
+{
+	Cash		res;
+
+	if (unlikely(pg_sub_s64_overflow(c1, c2, &res)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
+
+	return res;
+}
+
+static inline Cash
+cash_mul_float8(Cash c, float8 f)
+{
+	float8		res = rint(float8_mul((float8) c, f));
+
+	if (unlikely(isnan(res) || !FLOAT8_FITS_IN_INT64(res)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
+
+	return (Cash) res;
+}
+
+static inline Cash
+cash_div_float8(Cash c, float8 f)
+{
+	float8		res = rint(float8_div((float8) c, f));
+
+	if (unlikely(isnan(res) || !FLOAT8_FITS_IN_INT64(res)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
+
+	return (Cash) res;
+}
+
+static inline Cash
+cash_mul_int64(Cash c, int64 i)
+{
+	Cash		res;
+
+	if (unlikely(pg_mul_s64_overflow(c, i, &res)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
+
+	return res;
+}
+
+static inline Cash
+cash_div_int64(Cash c, int64 i)
+{
+	if (unlikely(i == 0))
+		ereport(ERROR,
+				(errcode(ERRCODE_DIVISION_BY_ZERO),
+				 errmsg("division by zero")));
+
+	return c / i;
+}
+
 /* cash_in()
  * Convert a string to a cash data type.
  * Format is [$]###[,]###[.##]
@@ -612,11 +689,8 @@ cash_pl(PG_FUNCTION_ARGS)
 {
 	Cash		c1 = PG_GETARG_CASH(0);
 	Cash		c2 = PG_GETARG_CASH(1);
-	Cash		result;
-
-	result = c1 + c2;
 
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_pl_cash(c1, c2));
 }
 
 
@@ -628,11 +702,8 @@ cash_mi(PG_FUNCTION_ARGS)
 {
 	Cash		c1 = PG_GETARG_CASH(0);
 	Cash		c2 = PG_GETARG_CASH(1);
-	Cash		result;
-
-	result = c1 - c2;
 
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mi_cash(c1, c2));
 }
 
 
@@ -664,10 +735,8 @@ cash_mul_flt8(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	float8		f = PG_GETARG_FLOAT8(1);
-	Cash		result;
 
-	result = rint(c * f);
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_float8(c, f));
 }
 
 
@@ -679,10 +748,8 @@ flt8_mul_cash(PG_FUNCTION_ARGS)
 {
 	float8		f = PG_GETARG_FLOAT8(0);
 	Cash		c = PG_GETARG_CASH(1);
-	Cash		result;
 
-	result = rint(f * c);
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_float8(c, f));
 }
 
 
@@ -694,15 +761,8 @@ cash_div_flt8(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	float8		f = PG_GETARG_FLOAT8(1);
-	Cash		result;
 
-	if (f == 0.0)
-		ereport(ERROR,
-				(errcode(ERRCODE_DIVISION_BY_ZERO),
-				 errmsg("division by zero")));
-
-	result = rint(c / f);
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_div_float8(c, f));
 }
 
 
@@ -714,10 +774,8 @@ cash_mul_flt4(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	float4		f = PG_GETARG_FLOAT4(1);
-	Cash		result;
 
-	result = rint(c * (float8) f);
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_float8(c, (float8) f));
 }
 
 
@@ -729,10 +787,8 @@ flt4_mul_cash(PG_FUNCTION_ARGS)
 {
 	float4		f = PG_GETARG_FLOAT4(0);
 	Cash		c = PG_GETARG_CASH(1);
-	Cash		result;
 
-	result = rint((float8) f * c);
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_float8(c, (float8) f));
 }
 
 
@@ -745,15 +801,8 @@ cash_div_flt4(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	float4		f = PG_GETARG_FLOAT4(1);
-	Cash		result;
-
-	if (f == 0.0)
-		ereport(ERROR,
-				(errcode(ERRCODE_DIVISION_BY_ZERO),
-				 errmsg("division by zero")));
 
-	result = rint(c / (float8) f);
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_div_float8(c, (float8) f));
 }
 
 
@@ -765,10 +814,8 @@ cash_mul_int8(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	int64		i = PG_GETARG_INT64(1);
-	Cash		result;
 
-	result = c * i;
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_int64(c, i));
 }
 
 
@@ -780,10 +827,8 @@ int8_mul_cash(PG_FUNCTION_ARGS)
 {
 	int64		i = PG_GETARG_INT64(0);
 	Cash		c = PG_GETARG_CASH(1);
-	Cash		result;
 
-	result = i * c;
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_int64(c, i));
 }
 
 /* cash_div_int8()
@@ -794,16 +839,8 @@ cash_div_int8(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	int64		i = PG_GETARG_INT64(1);
-	Cash		result;
-
-	if (i == 0)
-		ereport(ERROR,
-				(errcode(ERRCODE_DIVISION_BY_ZERO),
-				 errmsg("division by zero")));
 
-	result = c / i;
-
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_div_int64(c, i));
 }
 
 
@@ -815,10 +852,8 @@ cash_mul_int4(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	int32		i = PG_GETARG_INT32(1);
-	Cash		result;
 
-	result = c * i;
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_int64(c, (int64) i));
 }
 
 
@@ -830,10 +865,8 @@ int4_mul_cash(PG_FUNCTION_ARGS)
 {
 	int32		i = PG_GETARG_INT32(0);
 	Cash		c = PG_GETARG_CASH(1);
-	Cash		result;
 
-	result = i * c;
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_int64(c, (int64) i));
 }
 
 
@@ -846,16 +879,8 @@ cash_div_int4(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	int32		i = PG_GETARG_INT32(1);
-	Cash		result;
-
-	if (i == 0)
-		ereport(ERROR,
-				(errcode(ERRCODE_DIVISION_BY_ZERO),
-				 errmsg("division by zero")));
-
-	result = c / i;
 
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_div_int64(c, (int64) i));
 }
 
 
@@ -867,10 +892,8 @@ cash_mul_int2(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	int16		s = PG_GETARG_INT16(1);
-	Cash		result;
 
-	result = c * s;
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_int64(c, (int64) s));
 }
 
 /* int2_mul_cash()
@@ -881,10 +904,8 @@ int2_mul_cash(PG_FUNCTION_ARGS)
 {
 	int16		s = PG_GETARG_INT16(0);
 	Cash		c = PG_GETARG_CASH(1);
-	Cash		result;
 
-	result = s * c;
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_int64(c, (int64) s));
 }
 
 /* cash_div_int2()
@@ -896,15 +917,8 @@ cash_div_int2(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	int16		s = PG_GETARG_INT16(1);
-	Cash		result;
 
-	if (s == 0)
-		ereport(ERROR,
-				(errcode(ERRCODE_DIVISION_BY_ZERO),
-				 errmsg("division by zero")));
-
-	result = c / s;
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_div_int64(c, (int64) s));
 }
 
 /* cashlarger()
diff --git a/src/test/regress/expected/money.out b/src/test/regress/expected/money.out
index 7fd4e31804..890ce1da63 100644
--- a/src/test/regress/expected/money.out
+++ b/src/test/regress/expected/money.out
@@ -528,3 +528,109 @@ SELECT '-92233720368547758.08'::money::numeric;
  -92233720368547758.08
 (1 row)
 
+-- Test overflow checks
+SELECT '92233720368547758.07'::money + '0.01'::money;
+ERROR:  money out of range
+SELECT '-92233720368547758.08'::money - '0.01'::money;
+ERROR:  money out of range
+SELECT '92233720368547758.07'::money * 2::int8;
+ERROR:  money out of range
+SELECT '-92233720368547758.08'::money * 2::int8;
+ERROR:  money out of range
+SELECT 2::int8 * '92233720368547758.07'::money ;
+ERROR:  money out of range
+SELECT 2::int8 * '-92233720368547758.08'::money;
+ERROR:  money out of range
+SELECT '92233720368547758.07'::money * 2::int4;
+ERROR:  money out of range
+SELECT '-92233720368547758.08'::money * 2::int4;
+ERROR:  money out of range
+SELECT 2::int4 * '92233720368547758.07'::money ;
+ERROR:  money out of range
+SELECT 2::int4 * '-92233720368547758.08'::money;
+ERROR:  money out of range
+SELECT '92233720368547758.07'::money * 2::int2;
+ERROR:  money out of range
+SELECT '-92233720368547758.08'::money * 2::int2;
+ERROR:  money out of range
+SELECT 2::int2 * '92233720368547758.07'::money ;
+ERROR:  money out of range
+SELECT 2::int2 * '-92233720368547758.08'::money;
+ERROR:  money out of range
+SELECT '92233720368547758.07'::money * 2::float8;
+ERROR:  money out of range
+SELECT '-92233720368547758.08'::money * 2::float8;
+ERROR:  money out of range
+SELECT 2::float8 * '92233720368547758.07'::money ;
+ERROR:  money out of range
+SELECT 2::float8 * '-92233720368547758.08'::money;
+ERROR:  money out of range
+SELECT '92233720368547758.07'::money * 2::float4;
+ERROR:  money out of range
+SELECT '-92233720368547758.08'::money * 2::float4;
+ERROR:  money out of range
+SELECT 2::float4 * '92233720368547758.07'::money ;
+ERROR:  money out of range
+SELECT 2::float4 * '-92233720368547758.08'::money;
+ERROR:  money out of range
+SELECT '1'::money / 1.175494e-38::float8;
+ERROR:  money out of range
+SELECT '-1'::money / 1.175494e-38::float8;
+ERROR:  money out of range
+SELECT '1'::money / 1.175494e-38::float4;
+ERROR:  money out of range
+SELECT '-1'::money / 1.175494e-38::float4;
+ERROR:  money out of range
+-- Test invalid multipliers/divisors
+SELECT '42'::money * 'inf'::float8;
+ERROR:  money out of range
+SELECT '42'::money * '-inf'::float8;
+ERROR:  money out of range
+SELECT '42'::money * 'nan'::float8;
+ERROR:  money out of range
+SELECT 'inf'::float8 * '42'::money;
+ERROR:  money out of range
+SELECT '-inf'::float8 * '42'::money;
+ERROR:  money out of range
+SELECT 'nan'::float8 * '42'::money;
+ERROR:  money out of range
+SELECT '42'::money / 'inf'::float8;
+ ?column? 
+----------
+    $0.00
+(1 row)
+
+SELECT '42'::money / '-inf'::float8;
+ ?column? 
+----------
+    $0.00
+(1 row)
+
+SELECT '42'::money / 'nan'::float8;
+ERROR:  money out of range
+SELECT '42'::money * 'inf'::float4;
+ERROR:  money out of range
+SELECT '42'::money * '-inf'::float4;
+ERROR:  money out of range
+SELECT '42'::money * 'nan'::float4;
+ERROR:  money out of range
+SELECT 'inf'::float4 * '42'::money;
+ERROR:  money out of range
+SELECT '-inf'::float4 * '42'::money;
+ERROR:  money out of range
+SELECT 'nan'::float4 * '42'::money;
+ERROR:  money out of range
+SELECT '42'::money / 'inf'::float4;
+ ?column? 
+----------
+    $0.00
+(1 row)
+
+SELECT '42'::money / '-inf'::float4;
+ ?column? 
+----------
+    $0.00
+(1 row)
+
+SELECT '42'::money / 'nan'::float4;
+ERROR:  money out of range
diff --git a/src/test/regress/sql/money.sql b/src/test/regress/sql/money.sql
index 81c92dd960..4422d2816b 100644
--- a/src/test/regress/sql/money.sql
+++ b/src/test/regress/sql/money.sql
@@ -135,3 +135,50 @@ SELECT '12345678901234567'::money::numeric;
 SELECT '-12345678901234567'::money::numeric;
 SELECT '92233720368547758.07'::money::numeric;
 SELECT '-92233720368547758.08'::money::numeric;
+
+-- Test overflow checks
+SELECT '92233720368547758.07'::money + '0.01'::money;
+SELECT '-92233720368547758.08'::money - '0.01'::money;
+SELECT '92233720368547758.07'::money * 2::int8;
+SELECT '-92233720368547758.08'::money * 2::int8;
+SELECT 2::int8 * '92233720368547758.07'::money ;
+SELECT 2::int8 * '-92233720368547758.08'::money;
+SELECT '92233720368547758.07'::money * 2::int4;
+SELECT '-92233720368547758.08'::money * 2::int4;
+SELECT 2::int4 * '92233720368547758.07'::money ;
+SELECT 2::int4 * '-92233720368547758.08'::money;
+SELECT '92233720368547758.07'::money * 2::int2;
+SELECT '-92233720368547758.08'::money * 2::int2;
+SELECT 2::int2 * '92233720368547758.07'::money ;
+SELECT 2::int2 * '-92233720368547758.08'::money;
+SELECT '92233720368547758.07'::money * 2::float8;
+SELECT '-92233720368547758.08'::money * 2::float8;
+SELECT 2::float8 * '92233720368547758.07'::money ;
+SELECT 2::float8 * '-92233720368547758.08'::money;
+SELECT '92233720368547758.07'::money * 2::float4;
+SELECT '-92233720368547758.08'::money * 2::float4;
+SELECT 2::float4 * '92233720368547758.07'::money ;
+SELECT 2::float4 * '-92233720368547758.08'::money;
+SELECT '1'::money / 1.175494e-38::float8;
+SELECT '-1'::money / 1.175494e-38::float8;
+SELECT '1'::money / 1.175494e-38::float4;
+SELECT '-1'::money / 1.175494e-38::float4;
+-- Test invalid multipliers/divisors
+SELECT '42'::money * 'inf'::float8;
+SELECT '42'::money * '-inf'::float8;
+SELECT '42'::money * 'nan'::float8;
+SELECT 'inf'::float8 * '42'::money;
+SELECT '-inf'::float8 * '42'::money;
+SELECT 'nan'::float8 * '42'::money;
+SELECT '42'::money / 'inf'::float8;
+SELECT '42'::money / '-inf'::float8;
+SELECT '42'::money / 'nan'::float8;
+SELECT '42'::money * 'inf'::float4;
+SELECT '42'::money * '-inf'::float4;
+SELECT '42'::money * 'nan'::float4;
+SELECT 'inf'::float4 * '42'::money;
+SELECT '-inf'::float4 * '42'::money;
+SELECT 'nan'::float4 * '42'::money;
+SELECT '42'::money / 'inf'::float4;
+SELECT '42'::money / '-inf'::float4;
+SELECT '42'::money / 'nan'::float4;
-- 
2.39.3 (Apple Git-146)

