ramensoftware/windhawk · error · SettingsError

value contains a NUL character

Error message

value contains a NUL character

What it means

IniTree::set_string rejects values containing an embedded NUL ('\0'). WritePrivateProfileStringW takes a NUL-terminated string, so everything after an embedded NUL would be silently dropped while the call still reports success, and the INI line format offers no encoding to escape a NUL. The write is refused rather than storing a silently truncated value.

Solutions

  1. Strip the value at the first NUL before writing: value.split('\0').next().unwrap_or("") — if truncation is acceptable.
  2. Store binary data through a binary-safe mechanism (e.g. base64-encode the value, or use set_binary/registry backend as appropriate).
  3. Validate/normalize inputs at the boundary so NUL-containing values never reach the settings API.
  4. Handle the SettingsError explicitly and inform the user the value cannot be stored in INI format.

Example fix

// before
let raw = c_buffer_to_string(); // "abc\0def"
settings.set_string("blob", raw)?; // Err: value contains a NUL character

// after
let clean = raw.split('\0').next().unwrap_or_default();
settings.set_string("blob", &base64_encode(clean.as_bytes()))?;
Defensive patterns

Strategy: validation

Validate before calling

fn value_is_ini_safe(v: &str) -> bool { !v.contains('\0') && !v.contains('\n') && !v.contains('\r') }
assert!(value_is_ini_safe(&value), "value not storable in INI");

Type guard

fn ini_safe_value(v: &str) -> Option<&str> {
    (!v.contains('\0') && !v.contains('\n') && !v.contains('\r')).then_some(v)
}

Try / catch

match ini.set_string("key", &value) {
    Err(e) if e.to_string().contains("NUL") => {
        let encoded = base64_encode(value.as_bytes());
        ini.set_string("key", &encoded)?;
    },
    r => r?,
}

Prevention

When it happens

Trigger: Calling set_string (or set_int/set_binary which delegate to it) with a value argument containing '\0' — typically binary data or strings copied from C buffers passed as text.

Common situations: Storing binary blobs as strings; values assembled from fixed-size buffers without trimming at the terminator; serializing structures that embed NUL padding into settings.

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/a908397dd7a855e7. Report an issue: GitHub.

Appendix: source

Thrown at src/windhawk-core/windows/src/ini.rs:187

impl SettingsTree for IniTree {
    fn get_string(&self, name: &str) -> Result<Option<String>, SettingsError> {
        self.check_name("get", name)?;
        get_profile_string(&self.file, &self.section, name)
            .map_err(|os| self.err("get", os, "GetPrivateProfileString"))
    }

    fn set_string(&mut self, name: &str, value: &str) -> Result<(), SettingsError> {
        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)))
    }

View on GitHub (pinned to 61d99ed8e1)