From 3f9c07fd7b1416f73743a25c8e188bea014df895 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Thu, 15 Oct 2026 08:43:23 -0700 Subject: [PATCH 1/7] settings.toml: reject numbers with leading zeros TOML does not allow leading zeros on decimal integers or on the integer part of floats (https://toml.io/en/v1.0.0#integer). strtol base 0 read 017 as octal 15, and values like 08 or 01.5 fell through to the float parser. They now return SETTINGS_ERR_BAD_VALUE like other invalid numbers. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/environment.rst | 3 ++- shared-bindings/supervisor/__init__.c | 3 +++ supervisor/shared/settings.c | 13 +++++++++++-- tests/circuitpython-manual/settings/code.py | 7 +++++++ tests/circuitpython-manual/settings/settings.toml | 7 +++++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/environment.rst b/docs/environment.rst index 2899a75aff8..cd61ea6a966 100644 --- a/docs/environment.rst +++ b/docs/environment.rst @@ -54,7 +54,8 @@ Details of the TOML language subset * 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``, ``011`` is 9, not 11. + no ``0o``, no ``0b``, no underscores ``1_000``. +* Leading zeros are not allowed: ``011`` is an error, not 9 or 11. * Only bare keys are supported. * Duplicate keys are not diagnosed. * Comments are allowed. diff --git a/shared-bindings/supervisor/__init__.c b/shared-bindings/supervisor/__init__.c index a331249998a..b2952c8cd85 100644 --- a/shared-bindings/supervisor/__init__.c +++ b/shared-bindings/supervisor/__init__.c @@ -384,6 +384,9 @@ MP_DEFINE_CONST_FUN_OBJ_KW(supervisor_set_usb_identification_obj, 0, supervisor_ //| The values are returned as Python ``True`` or ``False`` values. //| - ``float``: signed or unsigned decimal number with a ``.`` or exponent, or ``inf``, ``inf``, ``nan``. //| +//| Leading zeros are not allowed in integers or in the integer part of floats: +//| ``017`` and ``01.5`` are errors. +//| //| Example:: //| //| # settings.toml: diff --git a/supervisor/shared/settings.c b/supervisor/shared/settings.c index fe13a4e6416..ef002d7476e 100644 --- a/supervisor/shared/settings.c +++ b/supervisor/shared/settings.c @@ -393,6 +393,15 @@ settings_err_t settings_get_str(const char *key, char *value, size_t value_len) return result; } +// TOML does not allow leading zeros on decimal integers or on the integer part of floats. +// https://toml.io/en/v1.0.0#integer and https://toml.io/en/v1.0.0#float +static bool has_leading_zero(const char *str) { + if (*str == '+' || *str == '-') { + str++; + } + return str[0] == '0' && (unichar_isdigit(str[1]) || str[1] == '_'); +} + static settings_err_t get_int(const char *key, mp_int_t *value) { char buf[16]; bool quoted; @@ -400,7 +409,7 @@ static settings_err_t get_int(const char *key, mp_int_t *value) { if (result != SETTINGS_OK) { return result; } - if (quoted) { + if (quoted || has_leading_zero(buf)) { return SETTINGS_ERR_BAD_VALUE; } char *end; @@ -459,7 +468,7 @@ static settings_err_t get_float(const char *key, mp_float_t *value) { if (result != SETTINGS_OK) { return result; } - if (quoted) { + if (quoted || has_leading_zero(buf)) { return SETTINGS_ERR_BAD_VALUE; } diff --git a/tests/circuitpython-manual/settings/code.py b/tests/circuitpython-manual/settings/code.py index bebed3d11ae..2978cfb6128 100644 --- a/tests/circuitpython-manual/settings/code.py +++ b/tests/circuitpython-manual/settings/code.py @@ -27,10 +27,13 @@ def check(key, expected): check("int_val", 42) check("neg_int_val", -7) check("hex_int_val", 31) +check("zero_int_val", 0) +check("signed_zero_val", 0) check("bool_true", True) check("bool_false", False) check("float_val", 3.14) check("neg_float_val", -1.5) +check("frac_float_val", 0.5) check("sci_float_val", 6.626e-34) # math.nan and math.inf are not always available check("pos_inf", float("inf")) @@ -50,6 +53,10 @@ def check_bad(key): check_bad("bad_float_junk") # float with trailing garbage check_bad("bad_inf_junk") # inf with trailing characters check_bad("bad_lone_sign") # bare + with nothing after it +check_bad("bad_leading_zero") # TOML does not allow leading zeros +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 # 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 5df2d104afe..a7f8f062309 100644 --- a/tests/circuitpython-manual/settings/settings.toml +++ b/tests/circuitpython-manual/settings/settings.toml @@ -3,13 +3,20 @@ bad_word = hello bad_float_junk = 1.2abc bad_inf_junk = infoo bad_lone_sign = + +bad_leading_zero = 017 +bad_leading_zero_neg = -017 +bad_leading_zero_08 = 08 +bad_leading_zero_float = 01.5 int_val = 42 neg_int_val = -7 hex_int_val = 0x1F +zero_int_val = 0 +signed_zero_val = -0 bool_true = true bool_false = false float_val = 3.14 neg_float_val = -1.5 +frac_float_val = 0.5 sci_float_val = 6.626e-34 pos_inf = inf neg_inf = -inf From 0e44c83aff2cf33ca0b7e43a8ff320291b120b72 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Fri, 16 Oct 2026 09:18:27 -0700 Subject: [PATCH 2/7] settings.toml: test 0x000F, clarify underscore docs TOML allows leading zeros after the 0x prefix, so check that 0x000F still parses as 15. Note in environment.rst that TOML allows 0o, 0b and underscores, and only this parser does not. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/environment.rst | 1 + tests/circuitpython-manual/settings/code.py | 1 + tests/circuitpython-manual/settings/settings.toml | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/environment.rst b/docs/environment.rst index cd61ea6a966..f8b06415e1c 100644 --- a/docs/environment.rst +++ b/docs/environment.rst @@ -55,6 +55,7 @@ Details of the TOML language subset * 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. * 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/tests/circuitpython-manual/settings/code.py b/tests/circuitpython-manual/settings/code.py index 2978cfb6128..510f244dccc 100644 --- a/tests/circuitpython-manual/settings/code.py +++ b/tests/circuitpython-manual/settings/code.py @@ -27,6 +27,7 @@ def check(key, expected): check("int_val", 42) check("neg_int_val", -7) check("hex_int_val", 31) +check("hex_leading_zero_val", 15) # TOML allows leading zeros after 0x check("zero_int_val", 0) check("signed_zero_val", 0) check("bool_true", True) diff --git a/tests/circuitpython-manual/settings/settings.toml b/tests/circuitpython-manual/settings/settings.toml index a7f8f062309..fd97b4d29bf 100644 --- a/tests/circuitpython-manual/settings/settings.toml +++ b/tests/circuitpython-manual/settings/settings.toml @@ -10,6 +10,7 @@ bad_leading_zero_float = 01.5 int_val = 42 neg_int_val = -7 hex_int_val = 0x1F +hex_leading_zero_val = 0x000F zero_int_val = 0 signed_zero_val = -0 bool_true = true From 50ee27e1010f9f871b59c0add64042caf463be10 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Fri, 16 Oct 2026 09:31:34 -0700 Subject: [PATCH 3/7] settings.toml: allow underscores between digits TOML allows underscores between digits in integers and floats, such as 1_000 or 0x1234_abcd. Each underscore must have a digit on both sides. Before, strtol() stopped at the underscore, so 1_000 fell through to the float parser and came back as 1000.0. mp_parse_float_internal() skips any underscore, so 1__0, _1 and 1_ were also accepted. Remove underscores in place after checking each one, before parsing. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/environment.rst | 4 +-- shared-bindings/supervisor/__init__.c | 1 + supervisor/shared/settings.c | 31 +++++++++++++++++-- tests/circuitpython-manual/settings/code.py | 10 ++++++ .../settings/settings.toml | 10 ++++++ 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/docs/environment.rst b/docs/environment.rst index f8b06415e1c..b33f94cfba1 100644 --- a/docs/environment.rst +++ b/docs/environment.rst @@ -54,8 +54,8 @@ Details of the TOML language subset * 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. + no ``0o``, no ``0b``. TOML allows both, but this parser does not yet. +* Underscores between digits are allowed: ``1_000`` is 1000. * 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..b4bd83ac1d1 100644 --- a/shared-bindings/supervisor/__init__.c +++ b/shared-bindings/supervisor/__init__.c @@ -386,6 +386,7 @@ MP_DEFINE_CONST_FUN_OBJ_KW(supervisor_set_usb_identification_obj, 0, supervisor_ //| //| Leading zeros are not allowed in integers or in the integer part of floats: //| ``017`` and ``01.5`` are errors. +//| Underscores are allowed between digits: ``1_000`` is the integer 1000. //| //| Example:: //| diff --git a/supervisor/shared/settings.c b/supervisor/shared/settings.c index ef002d7476e..ebcc88d7268 100644 --- a/supervisor/shared/settings.c +++ b/supervisor/shared/settings.c @@ -402,6 +402,33 @@ static bool has_leading_zero(const char *str) { return str[0] == '0' && (unichar_isdigit(str[1]) || str[1] == '_'); } +// TOML allows underscores between digits, such as 1_000 or 0xdead_beef. +// https://toml.io/en/v1.0.0#integer and https://toml.io/en/v1.0.0#float +// Removes them in place. Returns false if an underscore is not between two digits. +static bool remove_underscores(char *str) { + const char *p = str; + if (*p == '+' || *p == '-') { + p++; + } + bool hex = p[0] == '0' && p[1] == 'x'; + char *out = str; + char prev = 0; + for (const char *in = str; *in; in++) { + if (*in == '_') { + bool prev_ok = hex ? unichar_isxdigit(prev) : unichar_isdigit(prev); + bool next_ok = hex ? unichar_isxdigit(in[1]) : unichar_isdigit(in[1]); + if (!prev_ok || !next_ok) { + return false; + } + } else { + *out++ = *in; + } + prev = *in; + } + *out = '\0'; + return true; +} + static settings_err_t get_int(const char *key, mp_int_t *value) { char buf[16]; bool quoted; @@ -409,7 +436,7 @@ static settings_err_t get_int(const char *key, mp_int_t *value) { if (result != SETTINGS_OK) { return result; } - if (quoted || has_leading_zero(buf)) { + if (quoted || has_leading_zero(buf) || !remove_underscores(buf)) { return SETTINGS_ERR_BAD_VALUE; } char *end; @@ -468,7 +495,7 @@ static settings_err_t get_float(const char *key, mp_float_t *value) { if (result != SETTINGS_OK) { return result; } - if (quoted || has_leading_zero(buf)) { + if (quoted || has_leading_zero(buf) || !remove_underscores(buf)) { return SETTINGS_ERR_BAD_VALUE; } diff --git a/tests/circuitpython-manual/settings/code.py b/tests/circuitpython-manual/settings/code.py index 510f244dccc..e20a61e312e 100644 --- a/tests/circuitpython-manual/settings/code.py +++ b/tests/circuitpython-manual/settings/code.py @@ -28,6 +28,9 @@ 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("underscore_int_val", 1000) # TOML allows underscores between digits +check("neg_underscore_int_val", -1000) +check("hex_underscore_val", 0x1234ABCD) check("zero_int_val", 0) check("signed_zero_val", 0) check("bool_true", True) @@ -36,6 +39,8 @@ def check(key, expected): check("neg_float_val", -1.5) check("frac_float_val", 0.5) check("sci_float_val", 6.626e-34) +check("underscore_float_val", 1000.5) +check("underscore_exp_val", 1e10) # math.nan and math.inf are not always available check("pos_inf", float("inf")) check("neg_inf", -float("inf")) @@ -58,6 +63,11 @@ 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_double_underscore") # each underscore needs a digit on both sides +check_bad("bad_leading_underscore") +check_bad("bad_trailing_underscore") +check_bad("bad_underscore_before_dot") +check_bad("bad_underscore_after_hex_prefix") # 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..19c5835679a 100644 --- a/tests/circuitpython-manual/settings/settings.toml +++ b/tests/circuitpython-manual/settings/settings.toml @@ -7,10 +7,18 @@ bad_leading_zero = 017 bad_leading_zero_neg = -017 bad_leading_zero_08 = 08 bad_leading_zero_float = 01.5 +bad_double_underscore = 1__000 +bad_leading_underscore = _1 +bad_trailing_underscore = 1_ +bad_underscore_before_dot = 1_.5 +bad_underscore_after_hex_prefix = 0x_1F int_val = 42 neg_int_val = -7 hex_int_val = 0x1F hex_leading_zero_val = 0x000F +underscore_int_val = 1_000 +neg_underscore_int_val = -1_000 +hex_underscore_val = 0x1234_abcd zero_int_val = 0 signed_zero_val = -0 bool_true = true @@ -19,6 +27,8 @@ float_val = 3.14 neg_float_val = -1.5 frac_float_val = 0.5 sci_float_val = 6.626e-34 +underscore_float_val = 1_000.5 +underscore_exp_val = 1e1_0 pos_inf = inf neg_inf = -inf pos_nan = nan From d14ad76217c7f5b03f579723b448293c847f8a24 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Fri, 16 Oct 2026 14:59:31 -0700 Subject: [PATCH 4/7] Revert "settings.toml: allow underscores between digits" This reverts commit 50ee27e10. The extra code is not worth it: we do not need to match the TOML underscore rules exactly. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/environment.rst | 4 +-- shared-bindings/supervisor/__init__.c | 1 - supervisor/shared/settings.c | 31 ++----------------- tests/circuitpython-manual/settings/code.py | 10 ------ .../settings/settings.toml | 10 ------ 5 files changed, 4 insertions(+), 52 deletions(-) diff --git a/docs/environment.rst b/docs/environment.rst index b33f94cfba1..f8b06415e1c 100644 --- a/docs/environment.rst +++ b/docs/environment.rst @@ -54,8 +54,8 @@ Details of the TOML language subset * Whitespace is allowed. * Only basic strings are supported, not triple-quoted strings. * Only integers supported by ``strtol()`` can be parsed: - no ``0o``, no ``0b``. TOML allows both, but this parser does not yet. -* Underscores between digits are allowed: ``1_000`` is 1000. + no ``0o``, no ``0b``, no underscores ``1_000``. + TOML allows all of these, but this parser does not yet. * 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 b4bd83ac1d1..b2952c8cd85 100644 --- a/shared-bindings/supervisor/__init__.c +++ b/shared-bindings/supervisor/__init__.c @@ -386,7 +386,6 @@ MP_DEFINE_CONST_FUN_OBJ_KW(supervisor_set_usb_identification_obj, 0, supervisor_ //| //| Leading zeros are not allowed in integers or in the integer part of floats: //| ``017`` and ``01.5`` are errors. -//| Underscores are allowed between digits: ``1_000`` is the integer 1000. //| //| Example:: //| diff --git a/supervisor/shared/settings.c b/supervisor/shared/settings.c index ebcc88d7268..ef002d7476e 100644 --- a/supervisor/shared/settings.c +++ b/supervisor/shared/settings.c @@ -402,33 +402,6 @@ static bool has_leading_zero(const char *str) { return str[0] == '0' && (unichar_isdigit(str[1]) || str[1] == '_'); } -// TOML allows underscores between digits, such as 1_000 or 0xdead_beef. -// https://toml.io/en/v1.0.0#integer and https://toml.io/en/v1.0.0#float -// Removes them in place. Returns false if an underscore is not between two digits. -static bool remove_underscores(char *str) { - const char *p = str; - if (*p == '+' || *p == '-') { - p++; - } - bool hex = p[0] == '0' && p[1] == 'x'; - char *out = str; - char prev = 0; - for (const char *in = str; *in; in++) { - if (*in == '_') { - bool prev_ok = hex ? unichar_isxdigit(prev) : unichar_isdigit(prev); - bool next_ok = hex ? unichar_isxdigit(in[1]) : unichar_isdigit(in[1]); - if (!prev_ok || !next_ok) { - return false; - } - } else { - *out++ = *in; - } - prev = *in; - } - *out = '\0'; - return true; -} - static settings_err_t get_int(const char *key, mp_int_t *value) { char buf[16]; bool quoted; @@ -436,7 +409,7 @@ static settings_err_t get_int(const char *key, mp_int_t *value) { if (result != SETTINGS_OK) { return result; } - if (quoted || has_leading_zero(buf) || !remove_underscores(buf)) { + if (quoted || has_leading_zero(buf)) { return SETTINGS_ERR_BAD_VALUE; } char *end; @@ -495,7 +468,7 @@ static settings_err_t get_float(const char *key, mp_float_t *value) { if (result != SETTINGS_OK) { return result; } - if (quoted || has_leading_zero(buf) || !remove_underscores(buf)) { + if (quoted || has_leading_zero(buf)) { return SETTINGS_ERR_BAD_VALUE; } diff --git a/tests/circuitpython-manual/settings/code.py b/tests/circuitpython-manual/settings/code.py index e20a61e312e..510f244dccc 100644 --- a/tests/circuitpython-manual/settings/code.py +++ b/tests/circuitpython-manual/settings/code.py @@ -28,9 +28,6 @@ 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("underscore_int_val", 1000) # TOML allows underscores between digits -check("neg_underscore_int_val", -1000) -check("hex_underscore_val", 0x1234ABCD) check("zero_int_val", 0) check("signed_zero_val", 0) check("bool_true", True) @@ -39,8 +36,6 @@ def check(key, expected): check("neg_float_val", -1.5) check("frac_float_val", 0.5) check("sci_float_val", 6.626e-34) -check("underscore_float_val", 1000.5) -check("underscore_exp_val", 1e10) # math.nan and math.inf are not always available check("pos_inf", float("inf")) check("neg_inf", -float("inf")) @@ -63,11 +58,6 @@ 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_double_underscore") # each underscore needs a digit on both sides -check_bad("bad_leading_underscore") -check_bad("bad_trailing_underscore") -check_bad("bad_underscore_before_dot") -check_bad("bad_underscore_after_hex_prefix") # 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 19c5835679a..fd97b4d29bf 100644 --- a/tests/circuitpython-manual/settings/settings.toml +++ b/tests/circuitpython-manual/settings/settings.toml @@ -7,18 +7,10 @@ bad_leading_zero = 017 bad_leading_zero_neg = -017 bad_leading_zero_08 = 08 bad_leading_zero_float = 01.5 -bad_double_underscore = 1__000 -bad_leading_underscore = _1 -bad_trailing_underscore = 1_ -bad_underscore_before_dot = 1_.5 -bad_underscore_after_hex_prefix = 0x_1F int_val = 42 neg_int_val = -7 hex_int_val = 0x1F hex_leading_zero_val = 0x000F -underscore_int_val = 1_000 -neg_underscore_int_val = -1_000 -hex_underscore_val = 0x1234_abcd zero_int_val = 0 signed_zero_val = -0 bool_true = true @@ -27,8 +19,6 @@ float_val = 3.14 neg_float_val = -1.5 frac_float_val = 0.5 sci_float_val = 6.626e-34 -underscore_float_val = 1_000.5 -underscore_exp_val = 1e1_0 pos_inf = inf neg_inf = -inf pos_nan = nan From 1c5e76a2d7cf32916e786d2334c73e0d3e35cec5 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Fri, 16 Oct 2026 14:59:42 -0700 Subject: [PATCH 5/7] settings.toml: simplify leading zero check Only a digit after the leading 0 needs to be rejected. Co-Authored-By: Claude Opus 5.5 (1M context) --- supervisor/shared/settings.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervisor/shared/settings.c b/supervisor/shared/settings.c index ef002d7476e..73580b7155e 100644 --- a/supervisor/shared/settings.c +++ b/supervisor/shared/settings.c @@ -399,7 +399,7 @@ static bool has_leading_zero(const char *str) { if (*str == '+' || *str == '-') { str++; } - return str[0] == '0' && (unichar_isdigit(str[1]) || str[1] == '_'); + return str[0] == '0' && unichar_isdigit(str[1]); } static settings_err_t get_int(const char *key, mp_int_t *value) { From 56b50532b3bc3c7ce43c954edc77710c787ceb3f Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Sat, 17 Oct 2026 16:15:27 -0400 Subject: [PATCH 6/7] 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 From 19b1ed07b02c5ffe1b57298d971f974aef108258 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sat, 17 Oct 2026 16:42:49 -0700 Subject: [PATCH 7/7] settings.toml: fix two cases parse_digits() accepted '@' and '`' were read as the digit 9, so 12@ parsed as 129. Stop at any byte below 'a' after case folding. Underscores were skipped inside \u escapes, so "\u0_41" decoded to "A". Read escape digits directly instead of through parse_digits(). This also drops a variable-length array. Fold the decimal leading-zero check into the existing prefix check and move has_leading_zero() under MICROPY_PY_BUILTINS_FLOAT, since only get_float() uses it now. Co-Authored-By: Claude Opus 5.5 (1M context) --- supervisor/shared/settings.c | 42 ++++++++++--------- tests/circuitpython-manual/settings/code.py | 2 + .../settings/settings.toml | 2 + 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/supervisor/shared/settings.c b/supervisor/shared/settings.c index 876552d6ed6..b658770f45f 100644 --- a/supervisor/shared/settings.c +++ b/supervisor/shared/settings.c @@ -200,9 +200,11 @@ static const char *parse_digits(const char *str, mp_uint_t base, mp_uint_t *valu } if ('0' <= dig && dig <= '9') { dig -= '0'; - } else { - // Lowercase; anything that is not a letter ends up >= base. + } else if ((dig | 0x20) >= 'a') { + // Lowercase; anything past 'z' ends up >= base. dig = (dig | 0x20) - 'a' + 10; + } else { + break; } if (dig >= base) { break; @@ -219,14 +221,13 @@ static const char *parse_digits(const char *str, mp_uint_t base, mp_uint_t *valu } static settings_err_t read_unicode_escape(file_arg *file_handle, int sz, vstr_t *vstr) { - char hex_buf[sz + 1]; + mp_uint_t c = 0; for (int i = 0; i < sz; i++) { - hex_buf[i] = get_next_byte(file_handle); - } - hex_buf[sz] = 0; - mp_uint_t c; - if (parse_digits(hex_buf, 16, &c) != &hex_buf[sz]) { - return SETTINGS_ERR_BAD_VALUE; + uint8_t hex = get_next_byte(file_handle); + if (!unichar_isxdigit(hex)) { + return SETTINGS_ERR_BAD_VALUE; + } + c = c * 16 + unichar_xdigit_value(hex); } if (c >= 0x110000) { return SETTINGS_ERR_UNICODE; @@ -425,15 +426,6 @@ settings_err_t settings_get_str(const char *key, char *value, size_t value_len) return result; } -// TOML does not allow leading zeros on decimal integers or on the integer part of floats. -// https://toml.io/en/v1.0.0#integer and https://toml.io/en/v1.0.0#float -static bool has_leading_zero(const char *str) { - if (*str == '+' || *str == '-') { - str++; - } - return str[0] == '0' && unichar_isdigit(str[1]); -} - static settings_err_t get_int(const char *key, mp_int_t *value) { char buf[16]; bool quoted; @@ -441,7 +433,7 @@ static settings_err_t get_int(const char *key, mp_int_t *value) { if (result != SETTINGS_OK) { return result; } - if (quoted || has_leading_zero(buf)) { + if (quoted) { return SETTINGS_ERR_BAD_VALUE; } // Accept a superset of TOML: a sign is also allowed before 0x, 0o and 0b, the prefix @@ -464,6 +456,9 @@ static settings_err_t get_int(const char *key, mp_int_t *value) { } if (base != 10) { str += 2; + } else if (unichar_isdigit(str[1])) { + // TOML does not allow leading zeros on decimal integers. + return SETTINGS_ERR_BAD_VALUE; } } mp_uint_t magnitude; @@ -519,6 +514,15 @@ settings_err_t settings_get_bool(const char *key, bool *value) { } #if MICROPY_PY_BUILTINS_FLOAT +// TOML does not allow leading zeros on decimal integers or on the integer part of floats. +// https://toml.io/en/v1.0.0#integer and https://toml.io/en/v1.0.0#float +static bool has_leading_zero(const char *str) { + if (*str == '+' || *str == '-') { + str++; + } + return str[0] == '0' && unichar_isdigit(str[1]); +} + static settings_err_t get_float(const char *key, mp_float_t *value) { char buf[48]; bool quoted; diff --git a/tests/circuitpython-manual/settings/code.py b/tests/circuitpython-manual/settings/code.py index ea83afeccc2..f5e41abfc43 100644 --- a/tests/circuitpython-manual/settings/code.py +++ b/tests/circuitpython-manual/settings/code.py @@ -64,6 +64,8 @@ def check_bad(key): 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 +check_bad("bad_digit_at") # '@' is not a digit +check_bad("bad_unicode_underscore") # no underscores in \u escapes # 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 6a836abea44..d52b70eb1df 100644 --- a/tests/circuitpython-manual/settings/settings.toml +++ b/tests/circuitpython-manual/settings/settings.toml @@ -9,6 +9,8 @@ bad_leading_zero_08 = 08 bad_leading_zero_float = 01.5 bad_hex_no_digits = 0x bad_hex_overflow = 0x1_0000_0000 +bad_digit_at = 12@ +bad_unicode_underscore = "\u0_41" int_val = 42 neg_int_val = -7 hex_int_val = 0x1F