ramensoftware/windhawk · error · SettingsError
GetPrivateProfileString
Error message
GetPrivateProfileString
What it means
IniTree::get_string wraps the Win32 GetPrivateProfileString call; when that call fails, the OS error is mapped into a SettingsError with the operation tag "get" and message "GetPrivateProfileString". It indicates the underlying Windows API could not read the requested value from the INI file (file access, path, or format problem).
Solutions
- Inspect the embedded OS error (os) in the SettingsError to find the root cause (path not found, access denied, etc.).
- Verify the INI file exists at self.file and the process has read permission to it.
- Check the file is not exclusively locked by another process (editor, antivirus, sync client).
- Fall back to defaults or the registry backend if the file is unreadable.
- Re-create a valid INI file if it is corrupt.
Example fix
// before: propagate raw failure
let v = ini.get_string("key")?;
// after: degrade gracefully
let v = match ini.get_string("key") {
Ok(v) => v,
Err(e) => { log::warn!("INI read failed: {e}"); None }
}; Defensive patterns
Strategy: fallback
Validate before calling
let ini_path = Path::new(&file);
if !ini_path.exists() { log::warn!("INI file missing: {}", ini_path.display()); }
// optionally check readability
std::fs::File::open(ini_path).map_err(|e| format!("INI unreadable: {e}"))?; Try / catch
let value = match ini.get_string("key") {
Ok(v) => v,
Err(e) => { log::warn!("GetPrivateProfileString failed: {e}"); default_value.into() }
}; Prevention
- Store INI files in user-writable locations, not protected system directories.
- Check file existence/permissions at startup and re-create defaults if needed.
- Avoid holding the INI open or letting sync/AV tools lock it during reads.
- Always provide defaults for every setting read from INI.
When it happens
Trigger: Any read via get_string on an IniTree — also reached indirectly through get_int, get_binary, and enum_values — where get_profile_string returns an OS error: the INI file cannot be opened/read, the path is invalid, or access is denied.
Common situations: INI file moved or deleted while the app runs; file locked by another process; insufficient permissions in Program Files-like locations; a redirected or UNC path that GetPrivateProfileString cannot resolve.
Understand the failure class
Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.
Related errors
- throw PortableSettingsException(error)
- value name contains a NUL
- value name
- value contains a NUL character
- RegDeleteValue
AI-assisted analysis of ramensoftware/windhawk@61d99ed8e1 (2026-09-12).
Data as JSON: /api/errors/2f3d028486594741.
Report an issue: GitHub.
Appendix: source
Thrown at src/windhawk-core/windows/src/ini.rs:174
/// A value name reaches the profile API as a `PCWSTR`, which ends at its
/// first NUL, so a name carrying one addresses a DIFFERENT value - the
/// prefix - with the call reporting success. The read and remove paths take
/// a name straight to Win32, so they guard it here; the write path refuses
/// the same name as one [`unrepresentable_name`] rejects, so one name gets
/// one answer whichever operation carries it, in both storage modes.
fn check_name(&self, op: &'static str, name: &str) -> Result<(), SettingsError> {
if name.contains('\0') {
return Err(self.err(op, 0, &format!("value name {name:?} contains a NUL")));
}
Ok(())
}
}
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 registryView on GitHub (pinned to 61d99ed8e1)