Merge pull request #6339 from ebraminio/hb-number-copy-free

[number] Make integer parsings copy free
diff --git a/src/hb-number.cc b/src/hb-number.cc
index b6e7d69..03d8db4 100644
--- a/src/hb-number.cc
+++ b/src/hb-number.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019  Ebrahim Byagowi
+ * Copyright © 2019-2026  Ebrahim Byagowi
  *
  *  This is part of HarfBuzz, a text shaping library.
  *
@@ -31,55 +31,116 @@
 #include "hb-number.hh"
 #include "hb-number-parser.hh"
 
-template<typename T, typename Func>
+bool
+hb_parse_double (const char **pp, const char *end, double *pv, bool whole_buffer)
+{
+  const char *pend = end;
+  double value = hb_strtod (*pp, &pend);
+  if (unlikely (*pp == pend)) return false;
+  *pv = value;
+  *pp = pend;
+  return !whole_buffer || end == pend;
+}
+
+/* Returns the digit value of c in the given base (2..36), or -1 if c
+ * isn't a digit in that base. */
+static int
+_hb_digit_value (char c, int base)
+{
+  int d;
+  if ('0' <= c && c <= '9')      d = c - '0';
+  else if ('a' <= c && c <= 'z') d = c - 'a' + 10;
+  else if ('A' <= c && c <= 'Z') d = c - 'A' + 10;
+  else return -1;
+  return d < base ? d : -1;
+}
+
+/* Consumes digits in [p, pe) into *value (magnitude).  Sets *overflow
+ * if the value would exceed UINT64_MAX.  Sets *any_digits if at least
+ * one digit was consumed.  Returns the position past the last digit. */
+static const char *
+_hb_scan_digits (const char *p, const char *pe, int base,
+                 uint64_t *value, bool *overflow, bool *any_digits)
+{
+  const uint64_t cutoff = UINT64_MAX / (unsigned) base;
+  const uint64_t cutlim = UINT64_MAX % (unsigned) base;
+  uint64_t v = 0;
+  bool ov = false, any = false;
+
+  for (; p < pe; p++)
+  {
+    int d = _hb_digit_value (*p, base);
+    if (d < 0) break;
+    any = true;
+    if (v > cutoff || (v == cutoff && (uint64_t) d > cutlim))
+      ov = true;
+    else
+      v = v * (unsigned) base + (uint64_t) d;
+  }
+
+  *value = v;
+  *overflow = ov;
+  *any_digits = any;
+  return p;
+}
+
+template<typename T, typename Postprocess>
 static bool
 _parse_number (const char **pp, const char *end, T *pv,
-	       bool whole_buffer, Func f)
+               bool whole_buffer, int base, Postprocess post)
 {
-  char buf[32];
-  unsigned len = hb_min (ARRAY_LENGTH (buf) - 1, (unsigned) (end - *pp));
-  strncpy (buf, *pp, len);
-  buf[len] = '\0';
+  const char *pend = end;
+  const char *p = *pp;
+  bool neg = false, any = false, overflow = false;
 
-  char *p = buf;
-  char *pend = p;
+  while (p < pend && ISSPACE (*p)) p++;
+  if (p < pend && (*p == '+' || *p == '-')) { neg = (*p == '-'); p++; }
 
-  errno = 0;
-  *pv = f (p, &pend);
-  if (unlikely (errno || p == pend ||
-		/* Check if consumed whole buffer if is requested */
-		(whole_buffer && pend - p != end - *pp)))
-    return false;
+  /* Skip an optional "0x"/"0X" prefix for base 16, but only when a hex
+   * digit follows — "0x" alone parses as the digit 0, matching strtoul. */
+  if (base == 16 && p + 2 < pend && p[0] == '0' &&
+      (p[1] == 'x' || p[1] == 'X') && _hb_digit_value (p[2], 16) >= 0)
+    p += 2;
 
-  *pp += pend - p;
+  uint64_t v = 0;
+  p = _hb_scan_digits (p, pend, base, &v, &overflow, &any);
+  if (!any || overflow) return false;
+
+  T out;
+  if (!post (v, neg, &out)) return false;
+  if (whole_buffer && p != pend) return false;
+
+  *pv = out;
+  *pp = p;
   return true;
 }
 
 bool
 hb_parse_int (const char **pp, const char *end, int *pv, bool whole_buffer)
 {
-  return _parse_number<int> (pp, end, pv, whole_buffer,
-			     [] (const char *p, char **end)
-			     { return strtol (p, end, 10); });
+  return _parse_number (pp, end, pv, whole_buffer, 10,
+                        [] (uint64_t v, bool neg, int *out) {
+    /* INT_MAX = 2^31-1, INT_MIN magnitude = 2^31 */
+    uint64_t limit = neg ? (uint64_t) INT_MAX + 1 : (uint64_t) INT_MAX;
+    if (v > limit) return false;
+    *out = (int) (neg ? -(int64_t) v : (int64_t) v);
+    return true;
+  });
 }
 
 bool
 hb_parse_uint (const char **pp, const char *end, unsigned *pv,
-	       bool whole_buffer, int base)
+               bool whole_buffer, int base)
 {
-  return _parse_number<unsigned> (pp, end, pv, whole_buffer,
-				  [base] (const char *p, char **end)
-				  { return strtoul (p, end, base); });
-}
-
-bool
-hb_parse_double (const char **pp, const char *end, double *pv, bool whole_buffer)
-{
-  const char *pend = end;
-  *pv = hb_strtod (*pp, &pend);
-  if (unlikely (*pp == pend)) return false;
-  *pp = pend;
-  return !whole_buffer || end == pend;
+  return _parse_number (pp, end, pv, whole_buffer, base,
+                        [] (uint64_t v, bool neg, unsigned *out) {
+    if (v > UINT_MAX) return false;
+    /* Negate in unsigned so -1 wraps to UINT_MAX, matching strtoul
+     * truncated to unsigned. */
+    unsigned u = (unsigned) v;
+    *out = neg ? (unsigned) (0u - u) : u;
+    return true;
+  });
 }
 
 #endif /* HB_NUMBER_CC pacify */
diff --git a/src/test-number.cc b/src/test-number.cc
index e10efc8..7400820 100644
--- a/src/test-number.cc
+++ b/src/test-number.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019  Ebrahim Byagowi
+ * Copyright © 2019-2026  Ebrahim Byagowi
  *
  *  This is part of HarfBuzz, a text shaping library.
  *
@@ -46,6 +46,292 @@
   }
 
   {
+    const char str[] = "-123";
+    const char *pp = str;
+    const char *end = str + 4;
+
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pv == -123);
+    hb_always_assert (pp - str == 4);
+    hb_always_assert (end - pp == 0);
+    hb_always_assert (!*end);
+  }
+
+  /* Leading whitespace */
+  {
+    const char str[] = "   123";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pv == 123);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "\t123";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pv == 123);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "\n123";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pv == 123);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = " -123";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pv == -123);
+    hb_always_assert (pp == end);
+  }
+
+  /* Leading '+' sign */
+  {
+    const char str[] = "+123";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pv == 123);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "+0";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pv == 0);
+    hb_always_assert (pp == end);
+  }
+
+  /* Leading zeros are decimal, not octal (base is passed as 10) */
+  {
+    const char str[] = "007";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pv == 7);
+    hb_always_assert (pp == end);
+  }
+
+  /* Negative zero */
+  {
+    const char str[] = "-0";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pv == 0);
+    hb_always_assert (pp == end);
+  }
+
+  /* Partial parse: stops at first non-digit */
+  {
+    const char str[] = "123abc";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pv == 123);
+    hb_always_assert (pp == str + 3);
+  }
+
+  /* INT_MAX / INT_MIN (fits in long on every supported platform) */
+  {
+    const char str[] = "2147483647";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pv == 2147483647);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "-2147483648";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pv == -2147483647 - 1);
+    hb_always_assert (pp == end);
+  }
+
+  /* Failures: no digits */
+  {
+    const char str[] = "   ";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv = 99;
+    hb_always_assert (!hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 99);
+  }
+  {
+    const char str[] = "+";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv = 99;
+    hb_always_assert (!hb_parse_int (&pp, end, &pv));
+  }
+  {
+    const char str[] = "-";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv = 99;
+    hb_always_assert (!hb_parse_int (&pp, end, &pv));
+  }
+  {
+    const char str[] = "abc";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv = 99;
+    hb_always_assert (!hb_parse_int (&pp, end, &pv));
+  }
+
+  /* Multiple signs fail */
+  {
+    const char str[] = "++1";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv = 99;
+    hb_always_assert (!hb_parse_int (&pp, end, &pv));
+  }
+  {
+    const char str[] = "--1";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv = 99;
+    hb_always_assert (!hb_parse_int (&pp, end, &pv));
+  }
+  {
+    const char str[] = "+-1";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv = 99;
+    hb_always_assert (!hb_parse_int (&pp, end, &pv));
+  }
+  {
+    const char str[] = "-+1";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv = 99;
+    hb_always_assert (!hb_parse_int (&pp, end, &pv));
+  }
+
+  /* whole_buffer: leading whitespace ok, trailing content rejected,
+   * and *pp / *pv are left untouched on failure */
+  {
+    const char str[] = " 123";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv, true));
+    hb_always_assert (pv == 123);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "123abc";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv = 99;
+    hb_always_assert (!hb_parse_int (&pp, end, &pv, true));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 99);
+  }
+  {
+    /* 10 digits > INT_MAX.  The old strtol-based implementation silently
+     * truncated on 64-bit platforms (long is 64-bit) but rejected on
+     * 32-bit; the new implementation rejects uniformly. */
+    const char str[] = "9999999999";   /* 10 digits, > INT_MAX */
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv = 99;
+    hb_always_assert (!hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 99);
+  }
+
+  /* INT_MAX / INT_MIN - exactly representable */
+  {
+    const char str[] = "2147483647";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pv == 2147483647);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "-2147483648";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv;
+    hb_always_assert (hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pv == -2147483647 - 1);   /* not -2147483648, that's UB as an int literal */
+    hb_always_assert (pp == end);
+  }
+
+  /* One past INT_MAX - must fail */
+  {
+    const char str[] = "2147483648";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv = 99;
+    hb_always_assert (!hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 99);
+  }
+  /* One past INT_MIN - must fail */
+  {
+    const char str[] = "-2147483649";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv = 99;
+    hb_always_assert (!hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 99);
+  }
+
+  /* Same for signed */
+  {
+    const char str[] = "9999999999999999999999999999999999999999";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv = 99;
+    hb_always_assert (!hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 99);
+  }
+
+  {
+    /* Verify *pv and *pp are untouched when a range check rejects. */
+    const char str[] = "9999999999";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    int pv = 42;
+    hb_always_assert (!hb_parse_int (&pp, end, &pv));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 42);
+  }
+}
+
+static void
+test_parse_uint ()
+{
+  {
     const char str[] = "123";
     const char *pp = str;
     const char *end = str + strlen (str);
@@ -86,19 +372,6 @@
   }
 
   {
-    const char str[] = "-123";
-    const char *pp = str;
-    const char *end = str + 4;
-
-    int pv;
-    hb_always_assert (hb_parse_int (&pp, end, &pv));
-    hb_always_assert (pv == -123);
-    hb_always_assert (pp - str == 4);
-    hb_always_assert (end - pp == 0);
-    hb_always_assert (!*end);
-  }
-
-  {
     const char str[] = "123";
     const char *pp = str;
     hb_always_assert (ARRAY_LENGTH (str) == 4);
@@ -136,6 +409,410 @@
     hb_always_assert (pp - str == 3);
     hb_always_assert (end - pp == 2);
   }
+
+  {
+    const char str[] = "123abc";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv = 99;
+    hb_always_assert (!hb_parse_uint (&pp, end, &pv, true));
+    hb_always_assert (pp == str);
+    // hb_always_assert (pv == 99); this shouldn't fail but it does with the current implementation
+  }
+
+  /* Leading whitespace */
+  {
+    const char str[] = " 123";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv));
+    hb_always_assert (pv == 123);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "\t123";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv));
+    hb_always_assert (pv == 123);
+    hb_always_assert (pp == end);
+  }
+
+  /* Leading zeros (still decimal when base is 10) */
+  {
+    const char str[] = "007";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv));
+    hb_always_assert (pv == 7);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "000";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv));
+    hb_always_assert (pv == 0);
+    hb_always_assert (pp == end);
+  }
+
+  /* UINT_MAX */
+  {
+    const char str[] = "4294967295";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv));
+    hb_always_assert (pv == 4294967295u);
+    hb_always_assert (pp == end);
+  }
+
+  /* Negative wraps in two's complement (matches strtoul on unsigned) */
+  {
+    const char str[] = "-1";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv));
+    hb_always_assert (pv == 4294967295u);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "-0";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv = 99;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv));
+    hb_always_assert (pv == 0);
+    hb_always_assert (pp == end);
+  }
+
+  /* Base 2 */
+  {
+    const char str[] = "1010";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 2));
+    hb_always_assert (pv == 10);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "11111111";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 2));
+    hb_always_assert (pv == 255);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "12";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    /* '2' is not a base-2 digit; parse stops at index 1 */
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, false, 2));
+    hb_always_assert (pv == 1);
+    hb_always_assert (pp == str + 1);
+  }
+
+  /* Base 8 */
+  {
+    const char str[] = "777";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 8));
+    hb_always_assert (pv == 511);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "010";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 8));
+    hb_always_assert (pv == 8);
+    hb_always_assert (pp == end);
+  }
+
+  /* Base 16, with and without 0x prefix, mixed case */
+  {
+    const char str[] = "FF";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 16));
+    hb_always_assert (pv == 255);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "ff";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 16));
+    hb_always_assert (pv == 255);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "Ff";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 16));
+    hb_always_assert (pv == 255);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "0xFF";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 16));
+    hb_always_assert (pv == 255);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "0XFF";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 16));
+    hb_always_assert (pv == 255);
+    hb_always_assert (pp == end);
+  }
+
+  /* Base 36 */
+  {
+    const char str[] = "Z";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 36));
+    hb_always_assert (pv == 35);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "z";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 36));
+    hb_always_assert (pv == 35);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "10";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 36));
+    hb_always_assert (pv == 36);
+    hb_always_assert (pp == end);
+  }
+
+  /* Failures */
+  {
+    const char str[] = "abc";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv = 99;
+    /* 'a' isn't a decimal digit; whole parse fails */
+    hb_always_assert (!hb_parse_uint (&pp, end, &pv, false, 10));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 99);
+  }
+  {
+    const char str[] = "+";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv = 99;
+    hb_always_assert (!hb_parse_uint (&pp, end, &pv));
+  }
+  {
+    const char str[] = "-";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv = 99;
+    hb_always_assert (!hb_parse_uint (&pp, end, &pv));
+  }
+  {
+    const char str[] = "";
+    const char *pp = str;
+    const char *end = str;
+    unsigned int pv = 99;
+    hb_always_assert (!hb_parse_uint (&pp, end, &pv));
+  }
+
+  {
+    const char str[] = "0XFF";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 16));
+    hb_always_assert (pv == 255);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "0xff";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 16));
+    hb_always_assert (pv == 255);
+    hb_always_assert (pp == end);
+  }
+  {
+    const char str[] = "0x0";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 16));
+    hb_always_assert (pv == 0);
+    hb_always_assert (pp == end);
+  }
+  /* "0x" alone: consume only the 0, leave the x */
+  {
+    const char str[] = "0x";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, false, 16));
+    hb_always_assert (pv == 0);
+    hb_always_assert (pp == str + 1);
+  }
+  /* "0xG": 0 is consumed, xG left */
+  {
+    const char str[] = "0xG";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, false, 16));
+    hb_always_assert (pv == 0);
+    hb_always_assert (pp == str + 1);
+  }
+  /* Negative with prefix */
+  {
+    const char str[] = "-0xFF";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 16));
+    hb_always_assert (pv == (unsigned) -255);
+    hb_always_assert (pp == end);
+  }
+  /* Prefix rule doesn't fire for base 10 */
+  {
+    const char str[] = "0xFF";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, false, 10));
+    hb_always_assert (pv == 0);
+    hb_always_assert (pp == str + 1);
+  }
+  {
+    /* 10 digits > UINT_MAX.  The old strtol-based implementation silently
+     * truncated on 64-bit platforms (unsigned long is 64-bit) but rejected on
+     * 32-bit; the new implementation rejects uniformly. */
+    const char str[] = "9999999999";   /* 10 digits, > UINT_MAX */
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv = 99;
+    hb_always_assert (!hb_parse_uint (&pp, end, &pv));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 99);
+  }
+
+  /* UINT_MAX - exactly representable */
+  {
+    const char str[] = "4294967295";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv));
+    hb_always_assert (pv == 4294967295u);
+    hb_always_assert (pp == end);
+  }
+  /* One past UINT_MAX - must fail (currently truncates to 0) */
+  {
+    const char str[] = "4294967296";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv = 99;
+    hb_always_assert (!hb_parse_uint (&pp, end, &pv));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 99);
+  }
+
+  /* 20 digits, way past UINT_MAX */
+  {
+    const char str[] = "99999999999999999999";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv = 99;
+    hb_always_assert (!hb_parse_uint (&pp, end, &pv));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 99);
+  }
+  /* 40 digits, past the old 32-byte stack buffer */
+  {
+    const char str[] = "9999999999999999999999999999999999999999";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv = 99;
+    hb_always_assert (!hb_parse_uint (&pp, end, &pv));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 99);
+  }
+
+  /* 0xFFFFFFFF = UINT_MAX */
+  {
+    const char str[] = "0xFFFFFFFF";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv;
+    hb_always_assert (hb_parse_uint (&pp, end, &pv, true, 16));
+    hb_always_assert (pv == 4294967295u);
+    hb_always_assert (pp == end);
+  }
+  /* 0x100000000 = UINT_MAX + 1 */
+  {
+    const char str[] = "0x100000000";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv = 99;
+    hb_always_assert (!hb_parse_uint (&pp, end, &pv, true, 16));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 99);
+  }
+
+  /* 13 'Z's = 36^13 - 1, comfortably > UINT64_MAX would need ~12 digits */
+  {
+    const char str[] = "ZZZZZZZZZZZZZZ";   /* 14 Z's */
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv = 99;
+    hb_always_assert (!hb_parse_uint (&pp, end, &pv, true, 36));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 99);
+  }
+
+  {
+    const char str[] = "9999999999";
+    const char *pp = str;
+    const char *end = str + ARRAY_LENGTH (str) - 1;
+    unsigned int pv = 42;
+    hb_always_assert (!hb_parse_uint (&pp, end, &pv));
+    hb_always_assert (pp == str);
+    hb_always_assert (pv == 42);
+  }
 }
 
 /* Tolerate 1 ULP difference. Needed because 32-bit x86 (x87) computes
@@ -309,6 +986,7 @@
     const char *end = str + ARRAY_LENGTH (str) - 1;
     double pv = 99.0;
     hb_always_assert (!hb_parse_double (&pp, end, &pv));
+    hb_always_assert (pv == 99);
   }
   {
     const char str[] = ".";
@@ -316,6 +994,7 @@
     const char *end = str + ARRAY_LENGTH (str) - 1;
     double pv = 99.0;
     hb_always_assert (!hb_parse_double (&pp, end, &pv));
+    hb_always_assert (pv == 99);
   }
   {
     const char str[] = "1e-";
@@ -376,6 +1055,7 @@
     const char *end = str + ARRAY_LENGTH (str) - 1;
     double pv = 99.0;
     hb_always_assert (!hb_parse_double (&pp, end, &pv));
+    hb_always_assert (pv == 99.0);
   }
   {
     const char str[] = "";
@@ -383,6 +1063,7 @@
     const char *end = str;
     double pv = 99.0;
     hb_always_assert (!hb_parse_double (&pp, end, &pv));
+    hb_always_assert (pv == 99.0);
   }
   {
     const char str[] = " \t 123";
@@ -560,6 +1241,7 @@
     const char *end = str + ARRAY_LENGTH (str) - 1;
     double pv = 99.0;
     hb_always_assert (!hb_parse_double (&pp, end, &pv));
+    hb_always_assert (pv == 99.0);
   }
   {
     const char str[] = "+-1";
@@ -567,6 +1249,7 @@
     const char *end = str + ARRAY_LENGTH (str) - 1;
     double pv = 99.0;
     hb_always_assert (!hb_parse_double (&pp, end, &pv));
+    hb_always_assert (pv == 99.0);
   }
   {
     const char str[] = "0e5";
@@ -658,6 +1341,7 @@
     const char *end = str + ARRAY_LENGTH (str) - 1;
     double pv = 99.0;
     hb_always_assert (!hb_parse_double (&pp, end, &pv));
+    hb_always_assert (pv == 99.0);
   }
   {
     const char str[] = "inf";
@@ -665,6 +1349,7 @@
     const char *end = str + ARRAY_LENGTH (str) - 1;
     double pv = 99.0;
     hb_always_assert (!hb_parse_double (&pp, end, &pv));
+    hb_always_assert (pv == 99.0);
   }
   {
     const char str[] = "1..5";
@@ -691,6 +1376,7 @@
     const char *end = str + ARRAY_LENGTH (str) - 1;
     double pv = 99.0;
     hb_always_assert (!hb_parse_double (&pp, end, &pv));
+    hb_always_assert (pv == 99.0);
   }
   {
     const char str[] = " abc";
@@ -699,6 +1385,7 @@
     double pv = 99.0;
     hb_always_assert (!hb_parse_double (&pp, end, &pv));
     hb_always_assert (pp == str);
+    hb_always_assert (pv == 99.0);
   }
 
   {
@@ -733,8 +1420,9 @@
 int
 main (int argc, char **argv)
 {
-  test_parse_int ();
   test_parse_double ();
+  test_parse_int ();
+  test_parse_uint ();
 
   return 0;
 }