diff --git a/graalpython/com.oracle.graal.python.cext/src/longobject.c b/graalpython/com.oracle.graal.python.cext/src/longobject.c index 8d55014735..58eb4997e7 100644 --- a/graalpython/com.oracle.graal.python.cext/src/longobject.c +++ b/graalpython/com.oracle.graal.python.cext/src/longobject.c @@ -1,4 +1,4 @@ -/* Copyright (c) 2018, 2025, Oracle and/or its affiliates. +/* Copyright (c) 2018, 2026, Oracle and/or its affiliates. * Copyright (C) 1996-2017 Python Software Foundation * * Licensed under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 @@ -2453,6 +2453,7 @@ PyLong_FromString(const char *str, char **pend, int base) { int sign = 1, error_if_nonzero = 0; const char *start, *orig_str = str; + int orig_base = base; PyObject *z = NULL; PyObject *strobj; Py_ssize_t slen; @@ -2517,7 +2518,6 @@ PyLong_FromString(const char *str, char **pend, int base) long long result = strtoll(str, &endptr, base); if (error_if_nonzero && result != 0) { // let upcall handle the error reporting - base = 0; break; } // POSIX.1-2008: strtoll must not set errno on success, and set @@ -2538,11 +2538,53 @@ PyLong_FromString(const char *str, char **pend, int base) } } if (!z) { - z = GraalPyPrivate_Long_FromString((char *)orig_str, base); - if (z) { - // TODO: we should probably set the **pend out argument + z = GraalPyPrivate_Long_FromString(orig_str, orig_base); + if (!z && pend) { + /* + * We have an exception already, but we need to redo the validation + * to compute pend. Adapted from long_from_string_base + */ + *pend = (char *)str; + const char *end, *p; + char prev = 0; + start = p = str; + /* Leading underscore not allowed. */ + if (*start == '_') { + return NULL; + } + /* Verify all characters are digits and underscores. */ + while (_PyLong_DigitValue[Py_CHARMASK(*p)] < base || *p == '_') { + if (*p == '_') { + /* Double underscore not allowed. */ + if (prev == '_') { + *pend = (char *)(p - 1); + return NULL; + } + } + prev = *p; + ++p; + } + /* Trailing underscore not allowed. */ + if (prev == '_') { + *pend = (char *)(p - 1); + return NULL; + } + end = p; + *pend = (char *)end; + /* Reject empty strings */ + if (start == end) { + return NULL; + } + /* Allow only trailing whitespace after `end` */ + while (*p && Py_ISSPACE(*p)) { + p++; + } + *pend = (char *)p; } } + if (z && pend) { + *pend = (char *)(str + strlen(str)); + } return z; } diff --git a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py index cc5d718ca3..768771a9c3 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py @@ -40,7 +40,6 @@ from . import CPyExtTestCase, CPyExtFunction, CPyExtFunctionOutVars, unhandled_error_compare - int_bits = struct.calcsize('i') * 8 max_int = 2 ** (int_bits - 1) - 1 min_int = -2 ** (int_bits - 1) @@ -133,6 +132,22 @@ def _reference_is_compact(args): return 1 if -2147483648 <= n <= 2147483647 else 0 +def _reference_long_from_string(args): + num, base = args + if not num.strip(): + return ValueError, len(num) + try: + return int(num, base), len(num) + except Exception as e: + for i in range(len(num)): + try: + int(num[:i + 1], base) + except Exception: + if num[0] == '0' and base == 0: + i = len(num) + return type(e), i + + class DummyNonInt(): pass @@ -360,10 +375,11 @@ class TestPyLong(CPyExtTestCase): ) test_PyLong_FromString = CPyExtFunction( - lambda args: int(args[0], args[1]), + _reference_long_from_string, lambda: ( ("00", 0), ("03", 0), + ("0003", 0), (" 12 ", 10), (" 12abg13 ", 22), ("12", 0), @@ -371,10 +387,27 @@ class TestPyLong(CPyExtTestCase): ("0x132f1", 0), ("0x132132ff213213213231", 0), ("13123441234123423412341234123412341234124312341234213213213213213231", 0), + ("1312344123412342341234123412341234123x4124312341234213213213213213231", 0), + ("", 0), + (" ", 0), + ("123 ", 0), + ("-123 ", 0), + ("123 x", 0), + ("x", 0), + ("_1", 0), + ("1_", 0), + ("1_1", 0), + ("1__1", 0), ), code='''PyObject* wrap_PyLong_FromString(const char* str, int base) { char* pend; - return PyLong_FromString(str, &pend, base); + PyObject* val = PyLong_FromString(str, &pend, base); + if (!val) { + PyObject* exc = PyErr_GetRaisedException(); + val = Py_NewRef(Py_TYPE(exc)); + Py_DECREF(exc); + } + return Py_BuildValue("OL", val, pend - str); }''', callfunction="wrap_PyLong_FromString", resultspec="O", diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java index 6e994214b4..c92dc168a4 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java @@ -44,7 +44,7 @@ import static com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiCallPath.Direct; import static com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiCallPath.Ignored; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.CONST_UNSIGNED_CHAR_PTR; -import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.CharPtrAsTruffleString; +import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.ConstCharPtrAsTruffleString; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.ConstPyLongObject; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Int; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.LONG_LONG; @@ -74,8 +74,8 @@ import com.oracle.graal.python.builtins.objects.cext.capi.CExtNodes.CastToNativeLongNode; import com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor; import com.oracle.graal.python.builtins.objects.cext.common.CExtCommonNodes.ConvertPIntToPrimitiveNode; -import com.oracle.graal.python.builtins.objects.cext.common.CExtCommonNodes.TransformPExceptionToNativeCachedNode; import com.oracle.graal.python.builtins.objects.cext.common.CExtCommonNodesFactory.ConvertPIntToPrimitiveNodeGen; +import com.oracle.graal.python.builtins.objects.cext.common.CExtCommonNodes.TransformPExceptionToNativeCachedNode; import com.oracle.graal.python.builtins.objects.cext.structs.CStructAccess; import com.oracle.graal.python.builtins.objects.ints.IntBuiltins; import com.oracle.graal.python.builtins.objects.ints.IntNodes; @@ -221,7 +221,7 @@ static Object fromDouble(double d, } } - @CApiBuiltin(ret = PyObjectTransfer, args = {CharPtrAsTruffleString, Int}, call = Ignored) + @CApiBuiltin(ret = PyObjectTransfer, args = {ConstCharPtrAsTruffleString, Int}, call = Ignored) abstract static class GraalPyPrivate_Long_FromString extends CApiBinaryBuiltinNode { @Specialization