Avoid CHECK() in CFXJSE_FormCalcContext::Stuff()

Strict ordering of clamp() args may not be preserved with a zero-length
string. Then beware of start + count overflows.

-- Improve test coverage.

Bug: chromium:1284171
Change-Id: I4122aa2f77863c0b6c9e0021189fad57b7b000d2
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/88870
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Tom Sepez <tsepez@chromium.org>
diff --git a/fxjs/xfa/cfxjse_formcalc_context.cpp b/fxjs/xfa/cfxjse_formcalc_context.cpp
index 88d2616..0d2bf43 100644
--- a/fxjs/xfa/cfxjse_formcalc_context.cpp
+++ b/fxjs/xfa/cfxjse_formcalc_context.cpp
@@ -30,6 +30,7 @@
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/base/check.h"
 #include "third_party/base/cxx17_backports.h"
+#include "third_party/base/numerics/safe_conversions.h"
 #include "v8/include/v8-container.h"
 #include "v8/include/v8-function-callback.h"
 #include "v8/include/v8-object.h"
@@ -4297,31 +4298,35 @@
     return;
   }
 
-  ByteString bsSource;
-  ByteString bsInsert;
-  int32_t iLength = 0;
-  int32_t iStart = 0;
-  int32_t iDelete = 0;
   v8::Local<v8::Value> sourceValue = GetSimpleValue(info, 0);
   v8::Local<v8::Value> startValue = GetSimpleValue(info, 1);
   v8::Local<v8::Value> deleteValue = GetSimpleValue(info, 2);
-  if (!fxv8::IsNull(sourceValue) && !fxv8::IsNull(startValue) &&
-      !fxv8::IsNull(deleteValue)) {
-    bsSource = ValueToUTF8String(info.GetIsolate(), sourceValue);
-    iLength = bsSource.GetLength();
+  if (fxv8::IsNull(sourceValue) || fxv8::IsNull(startValue) ||
+      fxv8::IsNull(deleteValue)) {
+    info.GetReturnValue().SetNull();
+    return;
+  }
+
+  int32_t iStart = 1;  // one-based character indexing.
+  int32_t iDelete = 0;
+  ByteString bsSource = ValueToUTF8String(info.GetIsolate(), sourceValue);
+  int32_t iLength = pdfium::base::checked_cast<int32_t>(bsSource.GetLength());
+  if (iLength) {
     iStart = pdfium::clamp(
         static_cast<int32_t>(ValueToFloat(info.GetIsolate(), startValue)), 1,
         iLength);
-    iDelete = std::max(
-        0, static_cast<int32_t>(ValueToFloat(info.GetIsolate(), deleteValue)));
+    iDelete = pdfium::clamp(
+        static_cast<int32_t>(ValueToFloat(info.GetIsolate(), deleteValue)), 0,
+        iLength - iStart + 1);
   }
 
+  ByteString bsInsert;
   if (argc > 3) {
     v8::Local<v8::Value> insertValue = GetSimpleValue(info, 3);
     bsInsert = ValueToUTF8String(info.GetIsolate(), insertValue);
   }
 
-  --iStart;
+  --iStart;  // now zero-based.
   std::ostringstream szResult;
   int32_t i = 0;
   while (i < iStart) {
diff --git a/fxjs/xfa/cfxjse_formcalc_context_embeddertest.cpp b/fxjs/xfa/cfxjse_formcalc_context_embeddertest.cpp
index 19ab774..ab550e5 100644
--- a/fxjs/xfa/cfxjse_formcalc_context_embeddertest.cpp
+++ b/fxjs/xfa/cfxjse_formcalc_context_embeddertest.cpp
@@ -825,10 +825,41 @@
 TEST_F(CFXJSE_FormCalcContextEmbedderTest, Stuff) {
   ASSERT_TRUE(OpenDocument("simple_xfa.pdf"));
 
+  // Test wrong number of parameters.
+  ExecuteExpectError("Stuff(1, 2)");
+  ExecuteExpectError("Stuff(1, 2, 3, 4, 5)");
+
+  // Test null arguments.
+  ExecuteExpectNull("Stuff(null, 0, 4)");
+  ExecuteExpectNull("Stuff(\"ABCDEFG\", null, 4)");
+  ExecuteExpectNull("Stuff(\"ABCDEFG\", 0, null)");
+
+  // Insertions.
+  ExecuteExpectString("Stuff(\"\", 0, 0, \"clams\")", "clams");
   ExecuteExpectString("Stuff(\"TonyBlue\", 5, 0, \" \")", "Tony Blue");
+
+  // Deletions.
+  ExecuteExpectString("Stuff(\"A\", 1, 0)", "A");
+  ExecuteExpectString("Stuff(\"A\", 1, 1)", "");
   ExecuteExpectString("Stuff(\"ABCDEFGH\", 4, 2)", "ABCFGH");
-  ExecuteExpectString("Stuff(\"members-list@myweb.com\", 0, 0, \"cc:\")",
-                      "cc:members-list@myweb.com");
+  ExecuteExpectString("Stuff(\"ABCDEFGH\", 7, 2)", "ABCDEF");
+
+  // Test index clamping.
+  ExecuteExpectString("Stuff(\"ABCDEFGH\", -400, 400)", "");
+
+  // Need significant amount of text to test start + count overflow due to
+  // intermediate float representation of count not being able to hold
+  // INT_MAX.
+  ExecuteExpectString(
+      "Stuff(\""
+      "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678900"
+      "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678900"
+      "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678900"
+      "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678900"
+      "\", 133, 2147483520)",
+      "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678900"
+      "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678900"
+      "abcd");
 }
 
 TEST_F(CFXJSE_FormCalcContextEmbedderTest, Substr) {