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

  1. Strip or normalize line breaks from the value before calling set_string (e.g. replace '\r'/'\n' with a space).
  2. Encode the value (base64/hex) before storing and decode on read via get_binary/get_string.
  3. Store multi-line data in a separate file or the registry backend, which preserves such bytes faithfully.
  4. 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

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


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)