Skip to content

Reject settings.toml numbers with leading zeros, as TOML requires - #11620

Open
mikeysklar wants to merge 6 commits into
adafruit:mainfrom
mikeysklar:settings-toml-leading-zero
Open

mikeysklar wants to merge 6 commits into
adafruit:mainfrom
mikeysklar:settings-toml-leading-zero

Conversation

@mikeysklar

@mikeysklar mikeysklar commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

What

supervisor.get_setting() now raises ValueError for numbers like 017 or 01.5.

Why

017 was read as octal 15, and 08 came back as the float 8.0.

TOML does not allow leading zeros (Integer, Float).

settings.toml before after
017 15 ValueError
-017 -15 ValueError
00 0 ValueError
08 8.0 ValueError
01.5 1.5 ValueError
017.0 17.0 ValueError
007e1 70.0 ValueError
0, -0, 17, -17, +17, 0x11, 0x000F, 0.5 unchanged unchanged

Startup settings like CIRCUITPY_WEB_API_PORT now print "Invalid format" and keep their default, like any other bad number.

docs/environment.rst said 011 is 9, so that line is updated too.

Testing Result
Metro RP2040, 11.0.0-alpha.1 main + this PR (1c5e76a), Ubuntu host values above as listed
tests/circuitpython-manual/settings on that board all 26 PASS
unix coverage make test on 1c5e76a 951 passed

Out of scope: 0o17/0b10001 support, signed hex (-0x11) being accepted, underscores in ints (1_000 returns 1000.0), and the repeated inf and \v in the docstring.

Written with Claude Code, which also ran the tests above. I reviewed the code and this description.

🤖 Generated with Claude Code

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) <noreply@anthropic.com>

@dhalbert dhalbert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - couple of minor points.

Comment thread supervisor/shared/settings.c Outdated
if (*str == '+' || *str == '-') {
str++;
}
return str[0] == '0' && (unichar_isdigit(str[1]) || str[1] == '_');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

environnment.rst says underscores are not allowed, but you are checking for an underscore here.

Is the underscore restriction in the TOML spec or just an issue with our parser? I wonder if we can now handle underscores automatically.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Underscores between digits are allowed by the TOML spec, so I added support here. 1_000 is now the int 1000, and 1__0, _1, 1_ and 1_.5 are errors. The _ in the leading zero check stays because TOML never allows 0_17.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't an automatic option. Both MicroPython parsers skip _ anywhere without checking where it is, so they accept 1__0, _1 and 1_, which TOML forbids.

  • mp_parse_num_integer skips any _ (parsenum.c:109). It also raises and allocates, so it can't be used in settings.c.
  • mp_parse_float_internal skips any _ too (parsenum.c:367). That's why 1_000 used to come back as 1000.0.

So settings.c checks that each _ has a digit on both sides, removes the underscores, and then parses.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to match the TOML spec exactly. If the parsers we are using already accept underscores, just leave them be. Take out the remove_underscores(). It's extra code for basically no benefit. I was thinking that the line I have marked here could just be:

return str[0] == '0' && unichar_isdigit(str[1]);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: remove_underscores() is reverted and has_leading_zero() is now your one-liner. It saves 232 bytes on the Metro RP2040.

Tested on a Metro RP2040 with and without the helper. 017, 01.5 and 08 are still rejected. These change back to how main behaves:

settings.toml with helper now
1_000 1000 1000.0 (float)
0x1234_abcd 305441741 ValueError
0_17 ValueError 17.0
1__0 ValueError 10.0

So an int setting written with an underscore, like CIRCUITPY_WEB_API_PORT = 8_080, gives "Invalid format" and uses the default, same as on main.

Manual settings tests 26/26 and unix coverage 951/951 pass on 1c5e76a.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have pushed a PR to your branch with an alternate smaller implementation.

Comment thread tests/circuitpython-manual/settings/settings.toml
mikeysklar and others added 2 commits October 16, 2026 09:18
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@mikeysklar
mikeysklar requested a review from dhalbert October 16, 2026 21:18

@dhalbert dhalbert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What provoked this PR in the first place? Was there some case where someone had leading zeros and it was interpreted as octal?

Comment thread supervisor/shared/settings.c Outdated
if (*str == '+' || *str == '-') {
str++;
}
return str[0] == '0' && (unichar_isdigit(str[1]) || str[1] == '_');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to match the TOML spec exactly. If the parsers we are using already accept underscores, just leave them be. Take out the remove_underscores(). It's extra code for basically no benefit. I was thinking that the line I have marked here could just be:

return str[0] == '0' && unichar_isdigit(str[1]);

Comment thread supervisor/shared/settings.c Outdated
@mikeysklar

Copy link
Copy Markdown
Collaborator Author

This was part of the test fixture design for CircuitPython. A hardware test that checks settings.toml against values TOML rejects found that 017 was read as octal 15 on every board.

It seemed worth fixing before anyone hit it.

mikeysklar and others added 2 commits October 16, 2026 14:59
This reverts commit 50ee27e. 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) <noreply@anthropic.com>
Only a digit after the leading 0 needs to be rejected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikeysklar
mikeysklar requested a review from dhalbert October 16, 2026 22:27
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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants