main
Drew Owens 0.9.9 source: user-presence guard; adversary pass 45 (unreadable log unverifiable, latch keyed by pid+creation); manife… e772fef 20d ago
//! ADVERSARIAL REVIEW OF 0.9.7 — pass forty-five, the last of this cycle.
//!
//! readiness.rs, perflog.rs, quit.rs and the dispatch gate compile here and were
//! exercised; com.rs and LiveOs are cfg(windows): READ, NOT RUN.
//!
//! WHAT CHECKS OUT
//!  * The pass-44 findings pass, and the platform add-in is now keyed on the DLL
//!    (`swk3dcompassaddin.dll`), not a word.
//!  * Underscore stripping: no handler reads any underscore-prefixed key except
//!    the dispatcher's own `_startupNotVerified`, so stripping ab's injected
//!    `_callerBudgetMs` changes nothing in the bridge — ab enforces its budget on
//!    its own side. Nothing caller-facing is lost.
//!  * swx_dismiss_blocking_dialogs on the Connected notice: its words are
//!    template placeholders ("Title", "Description", "Button1"…), which
//!    `classify_words` maps to `unknown`, which is never answered — refused.
//!  * (2) The 60 s quiet window decides only the NO-MARKER paths. A session
//!    whose marker is after its last LOAD_ADDIN is `complete` however busy the
//!    log is, so DocSave-time <PROCMEM> lines cannot hold a healthy platform
//!    start. The one place a session's own activity can hold it is a session
//!    that never loaded the platform add-in while somebody works by hand,
//!    saving more often than once a minute — and the bridge's own document
//!    commands are refused before the first ready, so it cannot keep the log
//!    busy itself. Availability, narrow; noted, not tested. Clock skew and
//!    coarse mtime shift the 60 s by seconds; they cannot fake a marker.

mod common;
use common::*;
use serde_json::{json, Value};

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

fn session(scratch: &TempDir) {
    std::fs::write(
        scratch.join("mock-solidworks-open-docs.json"),
        json!({"running": true, "untitled": 1, "active": Value::Null, "docs": [], "files": {}}).to_string(),
    )
    .unwrap();
}

const HEADER: &str = "; SolidWorks generated log file";
const COMPASS: &str = r#"<AD2> "LOAD_ADDIN" "SWK3DCompassAddin.dll" "3DEXPERIENCE for WORKS" [2.18] "6.34.0.2027" 14 COM.new 1770885081 13"#;
const TOOLBOX: &str = r#"<AD2> "LOAD_ADDIN" "SWToolbox.dll" "SOLIDWORKS Toolbox Library" [1.0] "34.0" 14 COM.new 1770885070 13"#;
const MARKER: &str = "<3DXLOAD_TIME>24000</3DXLOAD_TIME>";

/// SEVERITY 2 — AN UNREADABLE LOG IS TREATED AS NO LOG, AND NO LOG LETS COM
/// DECIDE ALONE.
///
/// `startup_complete` returns the same `Unavailable` for "no log to read" and
/// for "the performance log … could not be read", and the verdict table maps
/// `(Unavailable, Some(true))` to READY. So a log that EXISTS but cannot be
/// opened — locked, an ACL, an antivirus holding it, a path the process cannot
/// reach — hands the verdict to COM, which was MEASURED reporting complete before
/// the marker on a real start. That is the 10-01 crash window by a different
/// door.
///
/// Absent and unreadable are different facts. "No log" on a machine whose
/// SOLIDWORKS never writes one can defensibly leave COM alone; "a log I could
/// not read" says the edition DOES write one and this bridge simply did not see
/// it. That is `unverifiable` — reads after the grace, mutations refused — the
/// same answer the bridge already gives when nothing ties the log to the
/// session.
#[test]
fn a_log_that_exists_but_cannot_be_read_does_not_let_com_decide_alone() {
    use std::os::unix::fs::PermissionsExt;
    let scratch = TempDir::new("adv43-unreadable");
    session(&scratch);
    let log = scratch.join("SolidWorksPerformance.log");
    std::fs::write(&log, format!("{HEADER}\n{TOOLBOX}\n{COMPASS}\n")).unwrap();
    std::fs::set_permissions(&log, std::fs::Permissions::from_mode(0o000)).unwrap();
    let unreadable = std::fs::read(&log).is_err();
    let logp = log.to_string_lossy().to_string();
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_PERF_LOG", &logp)]);
    b.call("swx_start", json!({}));
    let r = b.call("swx_new_document", json!({"type": "part"}));
    let _ = std::fs::set_permissions(&log, std::fs::Permissions::from_mode(0o644));
    assert!(unreadable, "precondition: this user cannot read the file (not running as root)");

    assert!(
        r["errorCode"] == json!("host_app_starting"),
        "the performance log EXISTS and could not be read — and its contents, had they been \
         readable, say a start is loading (the platform add-in loaded, no marker). The bridge \
         treated \"could not be read\" exactly like \"there is no log\" and let COM — measured true \
         before the marker — decide alone: swx_new_document ran. Unreadable is unverifiable, not \
         absent. {}",
        s(&r)
    );
}

/// SEVERITY 2 — THE READY LATCH IS KEYED BY PID ALONE, SO A NEW SESSION THAT
/// REUSES THE PID IS BORN READY.
///
/// `SESSIONS: HashMap<i64, Seen>` — the key is the process id; `seen.ready`, once
/// true, short-circuits every later assessment (`alreadyReadyThisSession`). The
/// process CREATION TIME is computed two lines earlier and not part of the key.
/// Windows reissues pids, and the map lives as long as the bridge server — so a
/// SOLIDWORKS quit and relaunched in one bridge lifetime that happens to get the
/// same pid skips the whole gate: no log, no COM, no grace. Ready at second 0 —
/// the crash window again. Everything else in this bridge that identifies a
/// session (session history, the quit ladder's `same_process`) keys on pid +
/// creation time; this is the one that does not.
///
/// It survives a com-host respawn (correct — the map is in the server), and a
/// server restart empties it (also correct). The fix is the key: (pid,
/// creation time).
#[test]
fn a_new_session_that_reuses_the_pid_does_not_inherit_ready() {
    let scratch = TempDir::new("adv43-pidreuse");
    session(&scratch);
    let log = scratch.join("SolidWorksPerformance.log");
    // Session one: a completed start, written since the (hour-old) process began.
    std::fs::write(&log, format!("{HEADER}\n{TOOLBOX}\n{COMPASS}\n{MARKER}\n")).unwrap();
    let logp = log.to_string_lossy().to_string();
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_PERF_LOG", &logp)]);
    let first = b.call("swx_start", json!({}));
    let pid1 = first["readiness"]["processId"].clone();
    let made = b.call("swx_new_document", json!({"type": "part"}));
    assert!(made["success"] == json!(true), "precondition: session one became ready: {}", s(&made));

    // Session two: SOLIDWORKS relaunched, the SAME pid reissued, created NOW,
    // and its log (rewritten at start) shows add-ins loading with no marker.
    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
    std::fs::write(scratch.join("mock-sldworks-created"), now.to_string()).unwrap();
    std::fs::write(&log, format!("{HEADER}\n{TOOLBOX}\n{COMPASS}\n")).unwrap();
    let st = b.call("swx_status", json!({}));
    let r = b.call("swx_new_document", json!({"type": "part"}));
    let text = s(&st);
    assert!(
        text.contains(&format!("\"processCreatedAt\": {now}")) && pid1.is_number(),
        "precondition: a new process (created {now}) with pid {pid1} — status: {text}"
    );

    assert!(
        r["errorCode"] == json!("host_app_starting"),
        "a NEW SOLIDWORKS process — created just now, its log showing add-ins loading and no marker \
         — reused the pid of a session the bridge had already seen ready, and the latch \
         (`alreadyReadyThisSession`, keyed by pid alone) waved swx_new_document through without \
         reading the log or COM. Key the latch by (pid, creation time), as every other session \
         identity in this bridge is keyed. {}",
        s(&r)
    );
}