{"record":{"id":"4bd6f152dcd419c4","repo":"flxzt/rnote","slug":"on-conflict-behaviour-is-still-on-conflict-after","errorCode":null,"errorMessage":"on-conflict behaviour is still {on_conflict} after applying overwrite.","messagePattern":"on-conflict behaviour is still (.+?) after applying overwrite\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/rnote-cli/src/export.rs","lineNumber":529,"sourceCode":"            let mut new_path = output_file.to_path_buf();\n            let Some(file_stem) = new_path\n                .file_stem()\n                .map(|s| s.to_string_lossy().to_string())\n            else {\n                return Err(anyhow::anyhow!(\"Failed to get file stem\"));\n            };\n            let ext = new_path\n                .extension()\n                .map(|n| n.to_string_lossy().to_string())\n                .unwrap_or_default();\n            while new_path.exists() {\n                i += 1;\n                new_path.set_file_name(format!(\"{file_stem}_{i}.{ext}\"))\n            }\n            Ok(Some(new_path))\n        }\n        OnConflict::AlwaysOverwrite | OnConflict::AlwaysSkip | OnConflict::AlwaysSuffix => {\n            Err(anyhow::anyhow!(\n                \"on-conflict behaviour is still {on_conflict} after applying overwrite.\"\n            ))\n        }\n    }\n}\n\npub(crate) async fn export_to_file(\n    engine: &mut Engine,\n    rnote_file: impl AsRef<Path>,\n    output_file: impl AsRef<Path>,\n    export_command: &cli::ExportCommand,\n    on_conflict: OnConflict,\n    on_conflict_overwrite: &mut Option<OnConflict>,\n    open: bool,\n) -> anyhow::Result<()> {\n    let rnote_bytes = cli::read_bytes_from_file(&rnote_file).await?;\n    let engine_snapshot = EngineSnapshot::load_from_rnote_bytes(rnote_bytes).await?;\n    let _ = engine.load_snapshot(engine_snapshot);","sourceCodeStart":511,"sourceCodeEnd":547,"githubUrl":"https://github.com/flxzt/rnote/blob/bbc5354502ba2fc83eec2670b535348825e679a6/crates/rnote-cli/src/export.rs#L511-L547","documentation":"Final defensive arm of `file_conflict_prompt_action` (crates/rnote-cli/src/export.rs:529): after the first match already normalized every `Always*` variant into its concrete counterpart (`AlwaysOverwrite`→`Overwrite`, `AlwaysSkip`→`Skip`, `AlwaysSuffix`→`Suffix`), the second match still observes an `Always*` value. That is impossible in the current flow, so this arm is an invariant guard that reports the leftover variant via the formatted message `on-conflict behaviour is still {on_conflict} after applying overwrite.`","triggerScenarios":"Unreachable with the current source; only fires if the normalization block (lines 489-500) is deleted or reordered, leaving an `AlwaysOverwrite`/`AlwaysSkip`/`AlwaysSuffix` value to fall through to the second match.","commonSituations":"Encountered by contributors refactoring the conflict-resolution code or by anyone patching the function and accidentally removing the `Always*` → concrete mapping; end users of stock rnote-cli builds should never see it.","solutions":["Treat as an internal invariant violation; verify the `Always*` normalization arms in the first match (lines 489-500) are intact.","If you refactored the function, re-add the mapping: `OnConflict::AlwaysOverwrite => { on_conflict = OnConflict::Overwrite; *on_conflict_overwrite = Some(on_conflict); }` and likewise for Skip/Suffix.","Simplify by merging the two matches so normalization and dispatch happen in one place, eliminating the possibility of `Always*` leaking through."],"exampleFix":"// before (refactor removed normalization, leaking Always*)\n// after: restore normalization before dispatch\nmatch on_conflict {\n    OnConflict::AlwaysOverwrite => { on_conflict = OnConflict::Overwrite; *on_conflict_overwrite = Some(on_conflict); }\n    OnConflict::AlwaysSkip => { on_conflict = OnConflict::Skip; *on_conflict_overwrite = Some(on_conflict); }\n    OnConflict::AlwaysSuffix => { on_conflict = OnConflict::Suffix; *on_conflict_overwrite = Some(on_conflict); }\n    _ => (),\n}","handlingStrategy":"validation","validationCode":"// Guard callers that construct OnConflict manually: normalize Always* before the API sees them.\nlet on_conflict = match on_conflict {\n    OnConflict::AlwaysOverwrite => OnConflict::Overwrite,\n    OnConflict::AlwaysSkip => OnConflict::Skip,\n    OnConflict::AlwaysSuffix => OnConflict::Suffix,\n    other => other,\n};","typeGuard":"fn is_always_variant(p: &OnConflict) -> bool {\n    matches!(p, OnConflict::AlwaysOverwrite | OnConflict::AlwaysSkip | OnConflict::AlwaysSuffix)\n}","tryCatchPattern":null,"preventionTips":["Keep the Always* normalization arms in the first match intact when refactoring.","Add a debug_assert! that the second match never receives an Always* variant.","Collapse the two sequential matches into one to make the invariant structural."],"tags":["rust","defensive-code","invariant","unreachable"],"backgroundTag":"internal-invariant-violation","analyzedSha":"bbc5354502ba2fc83eec2670b535348825e679a6","analyzedAt":"2026-09-08T13:20:33.747Z","contentChangedAt":"2026-09-08T13:20:33.747Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}