From 56b50532b3bc3c7ce43c954edc77710c787ceb3f Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Sat, 17 Oct 2026 16:15:27 -0400 Subject: [PATCH] settings.toml: parse integers without strtol() Replace strtol() in get_int() and strtoul() in read_unicode_escape() with a small digit parser. This drops strtol, strtoul, _ctype_, errno and _impure_data from the link: -660 bytes flash and -80 bytes RAM on the ItsyBitsy nRF52840, -620 bytes flash on the Feather RP2040. Integers now also accept 0o and 0b prefixes and underscores (1_000). As a superset of TOML, a sign is allowed before 0x/0o/0b, the prefix may be uppercase, and underscores are not checked to be between digits. Out-of-range integers are no longer clamped to LONG_MAX. settings_get_int() returns an error for them; supervisor.get_setting() raises ValueError for hex, octal and binary, and returns a float for decimal. Co-Authored-By: Claude Opus 5.5 --- docs/environment.rst | 7 +- shared-bindings/supervisor/__init__.c | 3 +- supervisor/shared/settings.c | 76 ++++++++++++++++--- tests/circuitpython-manual/settings/code.py | 6 ++ .../settings/settings.toml | 6 ++ 5 files changed, 85 insertions(+), 13 deletions(-) diff --git a/docs/environment.rst b/docs/environment.rst index f8b06415e1c..6424df4627d 100644 --- a/docs/environment.rst +++ b/docs/environment.rst @@ -53,9 +53,10 @@ Details of the TOML language subset * The supported data types are strings, integers, floats, and booleans. * Whitespace is allowed. * Only basic strings are supported, not triple-quoted strings. -* Only integers supported by ``strtol()`` can be parsed: - no ``0o``, no ``0b``, no underscores ``1_000``. - TOML allows all of these, but this parser does not yet. +* Integers may be decimal, hexadecimal ``0x1F``, octal ``0o17``, or binary ``0b101``, + and may contain underscores ``1_000``. + Unlike TOML, a sign is also allowed before ``0x``, ``0o``, and ``0b``, + the prefix may be uppercase, and underscores need not be between digits. * Leading zeros are not allowed: ``011`` is an error, not 9 or 11. * Only bare keys are supported. * Duplicate keys are not diagnosed. diff --git a/shared-bindings/supervisor/__init__.c b/shared-bindings/supervisor/__init__.c index b2952c8cd85..eed98d21faa 100644 --- a/shared-bindings/supervisor/__init__.c +++ b/shared-bindings/supervisor/__init__.c @@ -379,7 +379,8 @@ MP_DEFINE_CONST_FUN_OBJ_KW(supervisor_set_usb_identification_obj, 0, supervisor_ //| - ``str``: Double-quoted string. //| The string may include Unicode characters, and ``\\u`` Unicode escapes. Backslash-escaped characters //| ``\\b``, ``\\r``, ``\\n``, ``\\t``, ``\\v``, ``\\v`` are also allowed. -//| - ``int``: signed or unsigned integer. +//| - ``int``: signed or unsigned integer: decimal, or hexadecimal ``0x1F``, octal ``0o17``, +//| or binary ``0b101``. Underscores are allowed: ``1_000``. //| - ``bool``: lower-case ``true`` or ``false``. //| The values are returned as Python ``True`` or ``False`` values. //| - ``float``: signed or unsigned decimal number with a ``.`` or exponent, or ``inf``, ``inf``, ``nan``. diff --git a/supervisor/shared/settings.c b/supervisor/shared/settings.c index 73580b7155e..876552d6ed6 100644 --- a/supervisor/shared/settings.c +++ b/supervisor/shared/settings.c @@ -185,15 +185,47 @@ static bool key_matches(file_arg *file_handle, const char *key) { return true; } +// Parse digits in base 2 to 16, skipping any '_' after the first digit. +// Used instead of strtol() and strtoul(), which would pull in about 0.8 kB of libc. +// +// Returns a pointer past the last digit, or NULL if there are no digits or the value +// overflows mp_uint_t. *value is not touched on error. +static const char *parse_digits(const char *str, mp_uint_t base, mp_uint_t *value) { + const char *start = str; + mp_uint_t val = 0; + for (;; str++) { + mp_uint_t dig = (unsigned char)*str; + if (dig == '_' && str != start) { + continue; + } + if ('0' <= dig && dig <= '9') { + dig -= '0'; + } else { + // Lowercase; anything that is not a letter ends up >= base. + dig = (dig | 0x20) - 'a' + 10; + } + if (dig >= base) { + break; + } + if (__builtin_mul_overflow(val, base, &val) || __builtin_add_overflow(val, dig, &val)) { + return NULL; + } + } + if (str == start) { + return NULL; + } + *value = val; + return str; +} + static settings_err_t read_unicode_escape(file_arg *file_handle, int sz, vstr_t *vstr) { char hex_buf[sz + 1]; for (int i = 0; i < sz; i++) { hex_buf[i] = get_next_byte(file_handle); } hex_buf[sz] = 0; - char *end; - unsigned long c = strtoul(hex_buf, &end, 16); - if (end != &hex_buf[sz]) { + mp_uint_t c; + if (parse_digits(hex_buf, 16, &c) != &hex_buf[sz]) { return SETTINGS_ERR_BAD_VALUE; } if (c >= 0x110000) { @@ -412,15 +444,41 @@ static settings_err_t get_int(const char *key, mp_int_t *value) { if (quoted || has_leading_zero(buf)) { return SETTINGS_ERR_BAD_VALUE; } - char *end; - long num = strtol(buf, &end, 0); - while (unichar_isspace(*end)) { - end++; + // Accept a superset of TOML: a sign is also allowed before 0x, 0o and 0b, the prefix + // may be uppercase, and underscores need not be between digits. + const char *str = buf; + bool neg = false; + if (*str == '+' || *str == '-') { + neg = *str == '-'; + str++; + } + mp_uint_t base = 10; + if (str[0] == '0') { + char prefix = str[1] | 0x20; + if (prefix == 'x') { + base = 16; + } else if (prefix == 'o') { + base = 8; + } else if (prefix == 'b') { + base = 2; + } + if (base != 10) { + str += 2; + } + } + mp_uint_t magnitude; + str = parse_digits(str, base, &magnitude); + // A negative value can be one larger than MP_INT_MAX. + if (str == NULL || magnitude > (mp_uint_t)MP_INT_MAX + neg) { + return SETTINGS_ERR_BAD_VALUE; + } + while (unichar_isspace(*str)) { + str++; } - if (end == buf || *end) { // If the whole buffer was not consumed it's an error + if (*str) { // If the whole buffer was not consumed it's an error return SETTINGS_ERR_BAD_VALUE; } - *value = (mp_int_t)num; + *value = (mp_int_t)(neg ? 0 - magnitude : magnitude); return SETTINGS_OK; } diff --git a/tests/circuitpython-manual/settings/code.py b/tests/circuitpython-manual/settings/code.py index 510f244dccc..ea83afeccc2 100644 --- a/tests/circuitpython-manual/settings/code.py +++ b/tests/circuitpython-manual/settings/code.py @@ -28,6 +28,10 @@ def check(key, expected): check("neg_int_val", -7) check("hex_int_val", 31) check("hex_leading_zero_val", 15) # TOML allows leading zeros after 0x +check("neg_hex_int_val", -17) # not TOML, but accepted +check("oct_int_val", 15) +check("bin_int_val", 17) +check("underscore_int_val", 1000) # used to fall back to float 1000.0 check("zero_int_val", 0) check("signed_zero_val", 0) check("bool_true", True) @@ -58,6 +62,8 @@ def check_bad(key): check_bad("bad_leading_zero_neg") # also after a sign check_bad("bad_leading_zero_08") # not octal, so it used to fall back to float check_bad("bad_leading_zero_float") # nor on the integer part of a float +check_bad("bad_hex_no_digits") # prefix with no digits +check_bad("bad_hex_overflow") # does not fit in a 32-bit int # Missing key returns default got = supervisor.get_setting("no_such_key", "default") diff --git a/tests/circuitpython-manual/settings/settings.toml b/tests/circuitpython-manual/settings/settings.toml index fd97b4d29bf..6a836abea44 100644 --- a/tests/circuitpython-manual/settings/settings.toml +++ b/tests/circuitpython-manual/settings/settings.toml @@ -7,10 +7,16 @@ bad_leading_zero = 017 bad_leading_zero_neg = -017 bad_leading_zero_08 = 08 bad_leading_zero_float = 01.5 +bad_hex_no_digits = 0x +bad_hex_overflow = 0x1_0000_0000 int_val = 42 neg_int_val = -7 hex_int_val = 0x1F hex_leading_zero_val = 0x000F +neg_hex_int_val = -0x11 +oct_int_val = 0o17 +bin_int_val = 0b10001 +underscore_int_val = 1_000 zero_int_val = 0 signed_zero_val = -0 bool_true = true