libnyanpasu/clash-nyanpasu · error

legacy verge snapshot must serialize as a mapping

Error message

legacy verge snapshot must serialize as a mapping

What it means

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`).

Source

Thrown at backend/tauri/src/bridge/verge.rs:461

    ) -> anyhow::Result<crate::state::TypedConfigPatchPlan> {
        super::typed_patches_from_legacy_patch(base, patch, legacy_clash)
    }

    pub(crate) fn route_patch(patch: &IVerge) -> LegacyVergePatchRoute {
        route_verge_patch(patch)
    }

    pub(crate) fn validate_patch(patch: &IVerge) -> anyhow::Result<()> {
        validate_verge_patch(patch)
    }
}

fn legacy_patch_between(previous: &IVerge, desired: &IVerge) -> anyhow::Result<IVerge> {
    let previous = serde_yaml::to_value(previous)?;
    let desired = serde_yaml::to_value(desired)?;
    let previous = previous
        .as_mapping()
        .ok_or_else(|| anyhow::anyhow!("legacy verge snapshot must serialize as a mapping"))?;
    let mut patch = desired
        .as_mapping()
        .ok_or_else(|| anyhow::anyhow!("legacy verge snapshot must serialize as a mapping"))?
        .clone();
    patch.retain(|key, value| previous.get(key) != Some(value));
    Ok(serde_yaml::from_value(serde_yaml::Value::Mapping(patch))?)
}

/// Pure classifier (infallible). Validation is delegated to `validate_verge_patch`
/// or to `feat::patch_verge`. The side-effect field set mirrors `feat::patch_verge`.
#[allow(deprecated)]
fn route_verge_patch(patch: &IVerge) -> LegacyVergePatchRoute {
    let legacy = patch.enable_service_mode.is_some()
        || patch.enable_tun_mode.is_some()
        || patch.enable_auto_launch.is_some()
        || patch.enable_system_proxy.is_some()
        || patch.system_proxy_bypass.is_some()
        || patch.enable_proxy_guard.is_some()

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Inspect the verge snapshot file on disk; if it is empty or not a YAML object, restore a valid mapping-shaped config.
  2. Verify `IVerge` still derives a struct/map-style Serialize impl; undo changes that make it serialize to a scalar, sequence, or null.
  3. Guard the snapshot loader to reject/replace non-mapping documents with a default `IVerge` before they reach `legacy_patch_between`.
  4. Add a debug assertion or unit test asserting `serde_yaml::to_value(IVerge::default()).is_mapping()`.

Example fix

// before: trusting the snapshot blindly
let previous = self.legacy_store.snapshot()?;

// after: validate shape before diffing
let previous = self.legacy_store.snapshot()?;
if serde_yaml::to_value(&previous)?.as_mapping().is_none() {
    return Err(anyhow::anyhow!("verge snapshot is not a YAML mapping; reset to defaults"));
}
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_yaml_mapping<T: serde::Serialize>(v: &T) -> anyhow::Result<()> {
    let val = serde_yaml::to_value(v)?;
    anyhow::ensure!(val.is_mapping(), "expected a YAML mapping, got: {:?}", val);
    Ok(())
}
// call before diffing
ensure_yaml_mapping(&previous)?;
ensure_yaml_mapping(&desired)?;

Type guard

fn is_yaml_mapping(v: &serde_yaml::Value) -> bool {
    matches!(v, serde_yaml::Value::Mapping(_))
}

Try / catch

match legacy_patch_between(&previous, &desired) {
    Err(e) if e.to_string().contains("must serialize as a mapping") => {
        // reset to defaults and retry once
        let defaults = IVerge::default();
        legacy_patch_between(&defaults, &desired)
    }
    other => other,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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`).

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/2dee9fb24bfbe03a. Report an issue: GitHub.