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
- Strip the value at the first NUL before writing: value.split('\0').next().unwrap_or("") — if truncation is acceptable.
- Store binary data through a binary-safe mechanism (e.g. base64-encode the value, or use set_binary/registry backend as appropriate).
- Validate/normalize inputs at the boundary so NUL-containing values never reach the settings API.
- 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
- Base64-encode or otherwise encode any value that may contain non-text bytes before storing.
- Never pass C buffers or binary blobs directly as INI string values.
- Validate values for NUL/newlines before every write, mirroring the library's checks.
- Keep values destined for INI strictly textual; use a binary-capable store for anything else.
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
- value name contains a NUL
- value name
- GetPrivateProfileString
- value contains a line break
- Initial settings arrays must contain at least one template…
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)