Skip to content

Fix panic when parsing with a multi-byte delimiter - #61

Merged
QEDK merged 1 commit into
QEDK:masterfrom
dualfroz:fix-multibyte-delimiter-panic
Sep 6, 2026
Merged

QEDK merged 1 commit into
QEDK:masterfrom
dualfroz:fix-multibyte-delimiter-panic

Conversation

@dualfroz

@dualfroz dualfroz commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Problem

Parsing input panics when a configured delimiter is a multi-byte UTF-8
character and a line uses that delimiter:

use configparser::ini::{Ini, IniDefault};

let mut defaults = IniDefault::default();
defaults.delimiters = vec!['\u{00a7}']; // section sign, 2 bytes in UTF-8

let mut config = Ini::new_from_defaults(defaults);
config.read(String::from("[s]\nkey\u{00a7}value\n")).unwrap();
// thread panicked: start byte index 4 is not a char boundary;
// it is inside '§' (bytes 3..5 of string)

delimiters is a public, user-configurable field, so this is reachable through
the public API with safe input.

Root cause

In Ini::parse (src/ini.rs), the key/value split computes the delimiter
position with str::find, which returns a byte offset, and then slices the
value starting one byte later:

let value = trimmed[delimiter + 1..].trim().to_owned();

delimiter + 1 assumes the delimiter occupies a single byte. When the
delimiter is a multi-byte character, delimiter + 1 lands inside that
character, and slicing a str on a non-char-boundary index panics. ASCII
delimiters such as the default = and : happen to be one byte, which is why
this went unnoticed.

Fix

Advance past the delimiter by its actual UTF-8 length instead of a hard-coded
1:

let delimiter_len = trimmed[delimiter..]
    .chars()
    .next()
    .map_or(1, char::len_utf8);
let value = trimmed[delimiter + delimiter_len..].trim().to_owned();

For single-byte delimiters this is identical to the previous behaviour.

Test

Added multibyte_delimiter_does_not_panic to tests/test.rs, which configures
a two-byte delimiter (\u{00a7}), parses key\u{00a7}value, and asserts the
value is read back correctly instead of panicking.

Ini::parse split the value using delimiter + 1, assuming the delimiter
occupies a single byte. With a multi-byte UTF-8 delimiter this offset lands
inside the delimiter character, and slicing the string there panics. Advance
by the delimiter's actual UTF-8 length instead.
@dualfroz
dualfroz force-pushed the fix-multibyte-delimiter-panic branch from 12c5dbd to 1842f3f Compare September 5, 2026 23:13
@QEDK
QEDK requested a balanced review from Copilot September 6, 2026 14:46
@QEDK QEDK added the bug Something isn't working label Sep 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The change is a correct, well-tested, narrowly-scoped bug fix, but repository conventions favor conservative human sign-off for parser-behavior changes affecting the public API.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes a panic in Ini::parse that occurs when a user configures a multi-byte UTF-8 delimiter (via the public IniDefault::delimiters field) and a parsed line uses it. The previous code sliced the value with trimmed[delimiter + 1..], hard-coding a single-byte advance past the delimiter; for a multi-byte delimiter, delimiter + 1 lands inside the character and slicing on a non-char boundary panics. The fix advances by the delimiter's actual UTF-8 length.

Changes:

  • Compute the delimiter's real UTF-8 length via trimmed[delimiter..].chars().next().map_or(1, char::len_utf8) and slice the value at delimiter + delimiter_len.
  • Add a regression test multibyte_delimiter_does_not_panic using a two-byte delimiter (\u{00a7}).
File summaries
File Description
src/ini.rs Advances past the matched delimiter by its UTF-8 byte length instead of a hard-coded 1, preventing a panic on non-char-boundary slicing.
tests/test.rs Adds a regression test that parses a line with a two-byte delimiter and asserts the value is read back correctly.

The fix is correct: trimmed.find(&self.delimiters[..]) returns the byte offset of the start of the matched delimiter char, which is always a valid char boundary, so chars().next() reliably yields that delimiter char and char::len_utf8 gives the exact number of bytes to skip. For single-byte (ASCII) delimiters like the default = and :, the behavior is unchanged. I confirmed there are no other delimiter + 1 slicing sites, and the test follows the file's existing conventions.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@QEDK
QEDK merged commit 5ca2a5b into QEDK:master Sep 6, 2026
3 checks passed
@QEDK

QEDK commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Thanks for your contribution @dualfroz Merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants