fix(credentials): quarantine corrupt auth profile store (#1514)

This commit is contained in:
Steven Enamakel
2026-05-11 23:30:20 -07:00
committed by GitHub
parent ba3e116609
commit 99d2015dfc
2 changed files with 71 additions and 7 deletions
+44 -7
View File
@@ -373,13 +373,23 @@ impl AuthProfilesStore {
return Ok(PersistedAuthProfiles::default());
}
let mut persisted: PersistedAuthProfiles =
serde_json::from_slice(&bytes).with_context(|| {
format!(
"Failed to parse auth profile store at {}",
self.path.display()
)
})?;
let mut persisted: PersistedAuthProfiles = match serde_json::from_slice(&bytes) {
Ok(p) => p,
Err(err) => {
let quarantined = quarantine_corrupt_store(&self.path)?;
let quarantined_file = quarantined
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("auth-profiles.corrupt");
tracing::warn!(
path_file = PROFILES_FILENAME,
quarantined_file = quarantined_file,
error = %err,
"[credentials] auth profile store unparseable; quarantined and reset to empty"
);
return Ok(PersistedAuthProfiles::default());
}
};
if persisted.schema_version == 0 {
persisted.schema_version = CURRENT_SCHEMA_VERSION;
@@ -598,6 +608,33 @@ pub fn profile_id(provider: &str, profile_name: &str) -> String {
format!("{}:{}", provider.trim(), profile_name.trim())
}
fn quarantine_corrupt_store(path: &Path) -> Result<PathBuf> {
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("auth-profiles");
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("json");
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
let mut candidate = parent.join(format!("{stem}.corrupt-{ts}.{ext}"));
let mut suffix = 0u32;
while candidate.exists() {
suffix += 1;
candidate = parent.join(format!("{stem}.corrupt-{ts}-{suffix}.{ext}"));
}
fs::rename(path, &candidate).with_context(|| {
format!(
"Failed to quarantine corrupt auth profile store {} -> {}",
path.display(),
candidate.display()
)
})?;
Ok(candidate)
}
#[cfg(test)]
#[path = "profiles_tests.rs"]
mod tests;
@@ -127,6 +127,33 @@ fn auth_profiles_data_default() {
assert!(data.active_profiles.is_empty());
}
#[test]
fn corrupt_store_is_quarantined_and_reset() {
let tmp = TempDir::new().unwrap();
let store = AuthProfilesStore::new(tmp.path(), false);
let path = store.path().to_path_buf();
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, b"{ not valid json").unwrap();
let data = store.load().unwrap();
assert!(data.profiles.is_empty());
assert_eq!(data.schema_version, CURRENT_SCHEMA_VERSION);
let parent = path.parent().unwrap();
let quarantined: Vec<_> = std::fs::read_dir(parent)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().contains(".corrupt-"))
.collect();
assert_eq!(quarantined.len(), 1, "expected one quarantined file");
let profile = AuthProfile::new_token("openai", "default", "tok".into());
store.upsert_profile(profile, true).unwrap();
let reloaded = store.load().unwrap();
assert_eq!(reloaded.profiles.len(), 1);
}
#[test]
fn remove_nonexistent_profile_returns_false() {
let tmp = TempDir::new().unwrap();