main
Drew Owens 0.9.0: bridge data moved out of the install cache (recovery survives updates), closed-in-place records, gates route thr… 5d02ca9 20d ago
//! ADVERSARIAL REVIEW OF 0.7.8 — pass thirty.
//!
//! THE MANIFEST IS SOUND ON EVERY POINT IT WAS BUILT FOR.
//!
//!   * written after EVERY save (verbs3.rs:826) and EVERY close (:895), so a
//!     bridge that dies mid-sweep leaves a record of everything decided up to
//!     that document;
//!   * atomic — `manifest.json.tmp` then `rename` — so a crash mid-write leaves
//!     the previous complete manifest, never a truncated one that the listing
//!     would believe;
//!   * a crash between `SaveAs3` returning and the rewrite leaves a file with no
//!     entry, and that falls to `reverified_entry`, which yields `verified: null`
//!     — UNKNOWN — for a file that merely passes marker and floor. It returns
//!     `false` only for the conclusive case. That is the right asymmetry and it
//!     is the one I asked for in pass 29;
//!   * a manifest whose `files` is not an array is rejected wholesale by the
//!     `.filter()`, so a partial hand-edit degrades to re-verification rather
//!     than being half-believed;
//!   * a file the manifest names and the folder no longer holds is reported
//!     `present:false`, `verified:false`.
//!
//! The JSON-lines protocol is also fine: `send` uses `serde_json::to_string`,
//! not `to_string_pretty`, so a `description` carrying quotes or newlines is
//! escaped into one line, and a BSTR is `from_utf16_lossy` before it is ever a
//! Rust string, so no invalid UTF-8 can reach the frame.
//!
//! What is left is one gap in each half, and they have the same shape: a fact
//! that was known is not carried to the place that needs it.

mod common;
use common::*;
use serde_json::json;

fn s(v: &serde_json::Value) -> String {
    serde_json::to_string_pretty(v).unwrap_or_default()
}

/// SEVERITY 1 — THE VERDICT IS NOT BOUND TO THE BYTES IT WAS A VERDICT ABOUT.
///
/// Entries are matched to files by `name` and nothing else. The manifest records
/// `recoveryBytes` — what the file weighed when the sweep vouched for it — and
/// the listing reads `bytes` live from `metadata()`. **The two are never
/// compared.** So a file that changed after the sweep is reported with the
/// sweep's `verified:true` beside its new size, and `howToOpen` says to open it.
///
/// This is the one case where a wrong answer costs the user work, because
/// `verified:true` in a manifest means the document WAS CLOSED — these bytes are
/// the only copy. The realistic causes are not adversarial: a disk that filled
/// after the sweep, an antivirus quarantine that truncates in place, a sync
/// client mid-upload, a cleanup tool. The listing already handles the file being
/// GONE (`present:false`). A file that was emptied is the same event and is
/// reported as a rescue.
///
/// It is also the answer to "can `source:\"manifest\"` be spoofed": the honest
/// version is not an attacker with write access to the user's scratch — that
/// person has better options — it is that `source:"manifest"` reads as
/// provenance while nothing binds the record to the file. Comparing the two
/// numbers the manifest and the directory already hold closes both, with no new
/// data and no hash: if `bytes != recoveryBytes`, the sweep's verdict is about a
/// file that no longer exists, so report `verified: null` and say the file
/// changed. That is exactly the `reverified` rule 0.7.8 already established.
#[test]
fn a_manifest_verdict_does_not_survive_the_file_changing_underneath_it() {
    let scratch = TempDir::new("adv29-bind");
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);

    let root = scratch.join("recovery").join("20260917-000000");
    std::fs::create_dir_all(&root).unwrap();
    let mut body = vec![0u8; 2_000_000];
    body[4..8].copy_from_slice(&[0x00, 0x00, 0x00, 0x04]);
    std::fs::write(root.join("bracket.SLDPRT"), &body).unwrap();
    std::fs::write(
        root.join("manifest.json"),
        json!({
            "manifestVersion": 1,
            "sweep": "20260917-000000",
            "files": [{
                "name": "bracket.SLDPRT",
                "recoveryPath": root.join("bracket.SLDPRT").to_string_lossy(),
                "title": "bracket.SLDPRT",
                "kind": "part",
                // VOUCHED FOR AT TWO MEGABYTES, and the document was CLOSED
                // against it. These bytes are the only copy.
                "recoveryBytes": 2_000_000,
                "signatureOk": true,
                "verified": true,
                "preserved": true,
                "closed": true,
            }],
        })
        .to_string(),
    )
    .unwrap();

    // The sweep is over. The disk fills, or a scanner quarantines in place, and
    // the file is truncated. Marker intact, 900 bytes of a 2 MB document.
    std::fs::write(root.join("bracket.SLDPRT"), &body[..900]).unwrap();

    let r = b.call("swx_recover_list", json!({}));
    let f = r["sweeps"][0]["files"][0].clone();
    assert!(f["source"] == json!("manifest"), "precondition: read from the manifest: {}", s(&f));
    assert!(f["bytes"] == json!(900), "precondition: the live size is read: {}", s(&f));

    assert!(
        f["verified"] != json!(true),
        "the listing reports verified:true for a file that is 900 bytes, using a verdict the \
         sweep reached when it was 2,000,000 — a number the manifest itself records as \
         recoveryBytes, right next to the live size this verb just read. Nothing compares them. \
         verified:true in a manifest means the DOCUMENT WAS CLOSED, so this file is the only copy \
         of that work and the user is being told it is intact. The verb already reports a file \
         that vanished as present:false; a file that was emptied is the same event. Entry: {}",
        s(&f)
    );
}

/// SEVERITY 2 — THE SWEEP RECORDS THE PROSE AND DROPS THE STRUCTURE.
///
/// 0.7.8 exists so a caller can read the translator's own HRESULT instead of
/// guessing from a sentence, and `sidecar_error` carries `e.data` through as
/// `hostError`. But `swx_close_all_safely` does not go through `sidecar_error`
/// for a host failure. It builds its own entry, at verbs3.rs:807 and again at
/// :881:
///
/// ```ignore
/// Err(e) => {
///     close_failed.push(json!({"why": format!("the com host errored: {}", e.message)}));
///     break 'sweep;
/// }
/// ```
///
/// `e.data` is discarded at the construction site, so no structured detail can
/// reach the caller from this verb no matter what the host sends. The same holds
/// at verbs3.rs:1408, :1753 and :2143, which build `{"why": e.message}` for
/// per-document failures.
///
/// It matters here more than in `swx_import`, for two reasons. The sweep is the
/// verb that runs when SOLIDWORKS is already misbehaving, so it is the one most
/// likely to meet an HRESULT worth reading. And its records are now written into
/// `manifest.json` and kept — so a prose-only verdict is not merely harder to
/// diagnose in the moment, it is the permanent record of why a document was not
/// preserved.
///
/// The test asks only that the field EXIST on a host-failure entry. It cannot be
/// null-because-the-host-sent-nothing here; it is absent because nothing
/// constructs it.
#[test]
fn a_sweep_that_fails_on_the_host_carries_the_structured_reason() {
    let scratch = TempDir::new("adv29-hosterr");
    std::fs::write(scratch.join("mock-com-timeout"), "").unwrap();
    let machine = TempDir::new("adv29-hosterr-machine");
    std::fs::write(machine.join("windows.json"), "[]").unwrap();
    let b = Bridge::start_in(
        TempDir::borrowed(&scratch.0),
        &[("SWX_LAUNCH_FAKE_DIR", &machine.path())],
    );

    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));
    let failed = r["closeFailed"].as_array().cloned().unwrap_or_default();
    assert!(!failed.is_empty(), "precondition: the host failed the sweep: {}", s(&r));
    let e = &failed[0];
    assert!(
        e["why"].as_str().unwrap_or("").contains("com host errored"),
        "precondition: this is the host-failure entry: {}",
        s(e)
    );

    assert!(
        e.get("hostError").is_some(),
        "the sweep's host-failure entry carries only prose. verbs3.rs:807 and :881 build \
         {{\"why\": format!(\"the com host errored: {{}}\", e.message)}} and discard e.data, so \
         the structured COM detail 0.7.8 was built to surface cannot reach the caller from this \
         verb whatever the host sends. This is the verb that runs when SOLIDWORKS is already \
         misbehaving, and since 0.7.8 its records are persisted into manifest.json — so a \
         prose-only verdict is the permanent record of why a document was not preserved. \
         Entry: {}",
        s(e)
    );
}