diff --git a/crates/settings/src/document.rs b/crates/settings/src/document.rs index d5c80025..4e0a16f6 100644 --- a/crates/settings/src/document.rs +++ b/crates/settings/src/document.rs @@ -36,18 +36,22 @@ pub fn new_document(version: u32) -> DocumentMut { /// is kept. This is what keeps a renamed enum variant from resetting the whole file. pub fn deserialize_sections(doc: &DocumentMut) -> (T, Vec) { let defaults = T::default(); - let Ok(mut merged) = toml_edit::ser::to_document(&defaults) else { + let Ok(base) = toml_edit::ser::to_document(&defaults) else { return (defaults, Vec::new()); }; + let mut merged = base.clone(); let mut errors = Vec::new(); for (key, item) in doc.iter() { if key == SCHEMA_VERSION_KEY { continue; } - let mut candidate = merged.clone(); + // Each section is tried against the defaults alone, never against the sections accepted + // before it, so a file of many keys costs the defaults plus that key per key instead of + // the whole document so far. + let mut candidate = base.clone(); candidate[key] = item.clone(); - match toml_edit::de::from_document::(candidate.clone()) { - Ok(_) => merged = candidate, + match toml_edit::de::from_document::(candidate) { + Ok(_) => merged[key] = item.clone(), Err(e) => errors.push(SectionError { section: key.to_owned(), message: e.to_string() }), } } diff --git a/crates/settings/tests/recovery.rs b/crates/settings/tests/recovery.rs index 4af90bb4..437d18cf 100644 --- a/crates/settings/tests/recovery.rs +++ b/crates/settings/tests/recovery.rs @@ -47,6 +47,23 @@ fn bad_value_only_resets_its_section() { assert!(config_path(tmp.path()).exists(), "the file is not moved aside"); } +#[test] +fn many_unknown_keys_load_in_linear_time() { + // Each key was once tried against every key accepted before it. + let tmp = tempfile::tempdir().unwrap(); + let mut text = String::from("schema_version = 2\n"); + for i in 0..20_000 { + text.push_str(&format!("unknown_{i} = \"{}\"\n", "x".repeat(64))); + } + text.push_str("[general]\nlanguage = \"de\"\n"); + write_config(tmp.path(), &text); + let started = std::time::Instant::now(); + let (store, report) = open_in(tmp.path()); + assert!(report.section_errors.is_empty(), "{report:?}"); + assert_eq!(store.get().general.language, "de"); + assert!(started.elapsed() < std::time::Duration::from_secs(20), "{:?}", started.elapsed()); +} + #[test] fn newer_schema_opens_read_only_and_never_overwrites() { let tmp = tempfile::tempdir().unwrap();