diff --git a/docs/environment.rst b/docs/environment.rst index 2899a75aff8..6424df4627d 100644 --- a/docs/environment.rst +++ b/docs/environment.rst @@ -53,8 +53,11 @@ 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``, ``011`` is 9, not 11. +* 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. * Comments are allowed. diff --git a/shared-bindings/supervisor/__init__.c b/shared-bindings/supervisor/__init__.c index a331249998a..eed98d21faa 100644 --- a/shared-bindings/supervisor/__init__.c +++ b/shared-bindings/supervisor/__init__.c @@ -379,11 +379,15 @@ 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``. //| +//| 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..b658770f45f 100644 --- a/supervisor/shared/settings.c +++ b/supervisor/shared/settings.c @@ -185,16 +185,49 @@ 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 if ((dig | 0x20) >= 'a') { + // Lowercase; anything past 'z' ends up >= base. + dig = (dig | 0x20) - 'a' + 10; + } else { + break; + } + 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]; + mp_uint_t c = 0; 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]) { - 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; @@ -403,15 +436,44 @@ static settings_err_t get_int(const char *key, mp_int_t *value) { if (quoted) { 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; + } else if (unichar_isdigit(str[1])) { + // TOML does not allow leading zeros on decimal integers. + return SETTINGS_ERR_BAD_VALUE; + } } - if (end == buf || *end) { // If the whole buffer was not consumed it's an error + 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; } - *value = (mp_int_t)num; + while (unichar_isspace(*str)) { + str++; + } + if (*str) { // If the whole buffer was not consumed it's an error + return SETTINGS_ERR_BAD_VALUE; + } + *value = (mp_int_t)(neg ? 0 - magnitude : magnitude); return SETTINGS_OK; } @@ -452,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; @@ -459,7 +530,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..f5e41abfc43 100644 --- a/tests/circuitpython-manual/settings/code.py +++ b/tests/circuitpython-manual/settings/code.py @@ -27,10 +27,18 @@ 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("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) 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 +58,14 @@ 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 +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 5df2d104afe..d52b70eb1df 100644 --- a/tests/circuitpython-manual/settings/settings.toml +++ b/tests/circuitpython-manual/settings/settings.toml @@ -3,13 +3,29 @@ 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 +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 +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 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