{"record":{"id":"2dee9fb24bfbe03a","repo":"libnyanpasu/clash-nyanpasu","slug":"legacy-verge-snapshot-must-serialize-as-a-mapping","errorCode":null,"errorMessage":"legacy verge snapshot must serialize as a mapping","messagePattern":"legacy verge snapshot must serialize as a mapping","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/tauri/src/bridge/verge.rs","lineNumber":461,"sourceCode":"    ) -> anyhow::Result<crate::state::TypedConfigPatchPlan> {\n        super::typed_patches_from_legacy_patch(base, patch, legacy_clash)\n    }\n\n    pub(crate) fn route_patch(patch: &IVerge) -> LegacyVergePatchRoute {\n        route_verge_patch(patch)\n    }\n\n    pub(crate) fn validate_patch(patch: &IVerge) -> anyhow::Result<()> {\n        validate_verge_patch(patch)\n    }\n}\n\nfn legacy_patch_between(previous: &IVerge, desired: &IVerge) -> anyhow::Result<IVerge> {\n    let previous = serde_yaml::to_value(previous)?;\n    let desired = serde_yaml::to_value(desired)?;\n    let previous = previous\n        .as_mapping()\n        .ok_or_else(|| anyhow::anyhow!(\"legacy verge snapshot must serialize as a mapping\"))?;\n    let mut patch = desired\n        .as_mapping()\n        .ok_or_else(|| anyhow::anyhow!(\"legacy verge snapshot must serialize as a mapping\"))?\n        .clone();\n    patch.retain(|key, value| previous.get(key) != Some(value));\n    Ok(serde_yaml::from_value(serde_yaml::Value::Mapping(patch))?)\n}\n\n/// Pure classifier (infallible). Validation is delegated to `validate_verge_patch`\n/// or to `feat::patch_verge`. The side-effect field set mirrors `feat::patch_verge`.\n#[allow(deprecated)]\nfn route_verge_patch(patch: &IVerge) -> LegacyVergePatchRoute {\n    let legacy = patch.enable_service_mode.is_some()\n        || patch.enable_tun_mode.is_some()\n        || patch.enable_auto_launch.is_some()\n        || patch.enable_system_proxy.is_some()\n        || patch.system_proxy_bypass.is_some()\n        || patch.enable_proxy_guard.is_some()","sourceCodeStart":443,"sourceCodeEnd":479,"githubUrl":"https://github.com/libnyanpasu/clash-nyanpasu/blob/f7dbce2997c633e484f54788035e770b3ee99773/backend/tauri/src/bridge/verge.rs#L443-L479","documentation":"Raised by `legacy_patch_between` when serializing an `IVerge` snapshot with serde_yaml does not produce a YAML mapping. The function diffs `previous` vs `desired` snapshots by comparing mapping entries, so it requires both to serialize as `serde_yaml::Value::Mapping`. A non-mapping result is an internal invariant violation (e.g. an `IVerge` that serializes to a scalar/sequence, or a serialization produced `Value::Null`).","triggerScenarios":"Calling `run_legacy_verge_mutation` (via `patch_verge_config`) where `serde_yaml::to_value(previous)` or `serde_yaml::to_value(desired)` yields a non-mapping value — practically only when `IVerge`'s Serialize impl is changed to a non-struct representation, or the snapshot deserialized into a value that serializes as null/scalar.","commonSituations":"After a refactor changing `IVerge` serde representation (e.g. `#[serde(transparent)]` over a scalar or a newtype over a sequence); snapshots loaded from a config file whose top level is not a mapping (e.g. an empty file becoming `Value::Null`).","solutions":["Inspect the verge snapshot file on disk; if it is empty or not a YAML object, restore a valid mapping-shaped config.","Verify `IVerge` still derives a struct/map-style Serialize impl; undo changes that make it serialize to a scalar, sequence, or null.","Guard the snapshot loader to reject/replace non-mapping documents with a default `IVerge` before they reach `legacy_patch_between`.","Add a debug assertion or unit test asserting `serde_yaml::to_value(IVerge::default()).is_mapping()`."],"exampleFix":"// before: trusting the snapshot blindly\nlet previous = self.legacy_store.snapshot()?;\n\n// after: validate shape before diffing\nlet previous = self.legacy_store.snapshot()?;\nif serde_yaml::to_value(&previous)?.as_mapping().is_none() {\n    return Err(anyhow::anyhow!(\"verge snapshot is not a YAML mapping; reset to defaults\"));\n}","handlingStrategy":"validation","validationCode":"fn ensure_yaml_mapping<T: serde::Serialize>(v: &T) -> anyhow::Result<()> {\n    let val = serde_yaml::to_value(v)?;\n    anyhow::ensure!(val.is_mapping(), \"expected a YAML mapping, got: {:?}\", val);\n    Ok(())\n}\n// call before diffing\nensure_yaml_mapping(&previous)?;\nensure_yaml_mapping(&desired)?;","typeGuard":"fn is_yaml_mapping(v: &serde_yaml::Value) -> bool {\n    matches!(v, serde_yaml::Value::Mapping(_))\n}","tryCatchPattern":"match legacy_patch_between(&previous, &desired) {\n    Err(e) if e.to_string().contains(\"must serialize as a mapping\") => {\n        // reset to defaults and retry once\n        let defaults = IVerge::default();\n        legacy_patch_between(&defaults, &desired)\n    }\n    other => other,\n}","preventionTips":["Keep `IVerge` as a struct/map-style serde representation; cover it with a serialization-shape unit test.","Reject non-mapping (e.g. empty) verge YAML files at load time and fall back to defaults.","Add `debug_assert!(serde_yaml::to_value(&iv).unwrap().is_mapping())` in snapshot code.","Validate user-editable config files after manual edits or migrations."],"tags":["rust","yaml","serde","invariant"],"backgroundTag":"internal-invariant-violation","analyzedSha":"f7dbce2997c633e484f54788035e770b3ee99773","analyzedAt":"2026-09-08T01:24:59.197Z","contentChangedAt":"2026-09-08T01:24:59.197Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}