ramensoftware/windhawk · error · SettingsError
value contains a line break
Error message
value contains a line break
What it means
The INI settings backend refuses to store a string value containing CR or LF characters. An INI file is line-oriented, so a value with a line break would be silently cut at the break on read-back, with the remainder parsed as extra lines of the mod's [Mod] config file. The write is refused rather than reported as done, mirroring the NUL refusal and matching the registry backend's behavior for the same request.
Solutions
- Strip or normalize line breaks from the value before calling set_string (e.g. replace '\r'/'\n' with a space).
- Encode the value (base64/hex) before storing and decode on read via get_binary/get_string.
- Store multi-line data in a separate file or the registry backend, which preserves such bytes faithfully.
- Use a delimiter-based encoding (e.g. join with a literal '\n' escape) and split on read.
Example fix
// before
settings.set_string("note", user_text)?; // fails if user_text has newlines
// after
let sanitized: String = user_text.chars().map(|c| if c == '\n' || c == '\r' { ' ' } else { c }).collect();
settings.set_string("note", &sanitized)?; Defensive patterns
Strategy: validation
Validate before calling
fn assert_ini_safe(v: &str) -> Result<(), String> {
if v.contains(['\r', '\n']) { Err("value contains a line break".into()) } else { Ok(()) }
} Type guard
fn is_ini_value_safe(v: &str) -> bool { !v.contains('\r') && !v.contains('\n') } Try / catch
match settings.set_string("k", &v) {
Err(e) if e.message().contains("line break") => { /* sanitize and retry */ }
r => r?,
} Prevention
- Sanitize all user-provided strings (strip/normalize CRLF) before storing.
- Prefer set_binary (hex) for values that may contain control characters.
- Add a unit test asserting settings values survive a set/get round-trip.
- Document that INI values are single-line by design.
When it happens
Trigger: Calling set_string (directly or via set_int/set_binary wrappers) with a value containing '\r' or '\n', e.g. storing a multi-line note, a pasted block of text, or a string built from user input that includes newlines.
Common situations: Storing multi-line user comments or regex patterns with embedded newlines; serializing multi-line log excerpts into a mod setting; migrating values from a registry-backed store (where line breaks were stored) into the INI backend.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- value name contains a NUL
- value name
- value contains a NUL character
- Initial settings arrays must contain at least one template…
- Invalid object array schema definition.
AI-assisted analysis of ramensoftware/windhawk@61d99ed8e1 (2026-09-12).
Data as JSON: /api/errors/4fc5f0db37ea1fa6.
Report an issue: GitHub.
Appendix: source
Thrown at src/windhawk-core/windows/src/ini.rs:196
if let Some(why) = unrepresentable_name(name) {
return Err(self.err("set", 0, &format!("value name {name:?} {why}")));
}
// `WritePrivateProfileStringW` takes the value as a NUL-terminated
// string, so an embedded NUL ends it: everything after it is dropped and
// the call still reports success. `escape_ini_value` cannot rescue that
// (the INI line format has no encoding for a NUL), so refuse the write
// rather than store a silently truncated value.
if value.contains('\0') {
return Err(self.err("set", 0, "value contains a NUL character"));
}
// An INI entry ends at the line break, so a value carrying one cannot
// be stored either: it would read back cut at the break, with the rest
// of it parsed as further lines of a file that also holds the mod's
// `[Mod]` config. Refused for the same reason as a NUL - the registry
// backend stores both halves of such a value faithfully, and a write
// that cannot keep the value is better refused than reported as done.
if value.contains(['\r', '\n']) {
return Err(self.err("set", 0, "value contains a line break"));
}
let escaped = escape_ini_value(value);
write_profile(&self.file, &self.section, Some(name), Some(&escaped))
}
fn get_int(&self, name: &str) -> Result<Option<i32>, SettingsError> {
Ok(self.get_string(name)?.map(|s| parse_c_int(&s)))
}
fn set_int(&mut self, name: &str, value: i32) -> Result<(), SettingsError> {
// SetInt -> SetString(to_wstring(value)); a decimal triggers no
// escaping, but route through set_string for exactness.
self.set_string(name, &value.to_string())
}
fn get_binary(&self, name: &str) -> Result<Option<Vec<u8>>, SettingsError> {
match self.get_string(name)? {
None => Ok(None),View on GitHub (pinned to 61d99ed8e1)