From b74a3fd208605bef81ac8d76bc9caa2c7d6254f2 Mon Sep 17 00:00:00 2001 From: MrBeldum <157205701+MrBeldum@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:09:27 -0700 Subject: [PATCH] Test the update-push headers in both directions A 4.1 Mac sends both header name sets so a 4.0 host accepts the push, and a 4.1 host accepts a push from a 4.0 Mac that sends only the old names. Neither direction had a test, and renaming the constant in legacy.rs would have broken the first without a failure. The header list is now a function the test reads. Co-Authored-By: Claude Sonnet 5.5 --- crates/client/src/update.rs | 36 +++++++++++++++++++++++++++--------- crates/host/src/update.rs | 22 ++++++++++++++++++++++ 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/crates/client/src/update.rs b/crates/client/src/update.rs index 2421258..7dccc64 100644 --- a/crates/client/src/update.rs +++ b/crates/client/src/update.rs @@ -590,20 +590,25 @@ fn runs_at_least(addr: SocketAddr, want: &Version, within: Duration) -> bool { } } +/// The headers of a host-update push: this version's names and, because a +/// host older than 4.1 knows only the old ones and is the one this push is +/// most likely for, the old names too. +fn push_headers<'a>(version: &'a str, sha: &'a str) -> [(&'static str, &'a str); 5] { + [ + (UPDATE_VERSION_HEADER, version), + (UPDATE_SHA256_HEADER, sha), + (legacy::UPDATE_VERSION_HEADER, version), + (legacy::UPDATE_SHA256_HEADER, sha), + ("Content-Type", "application/octet-stream"), + ] +} + fn send_host(ip: Ipv4Addr, version: &str, sha: &str, exe: &[u8]) -> Result<()> { let r = http::request_with( (ip, CONTROL_PORT), "POST", UPDATE_PATH, - &[ - (UPDATE_VERSION_HEADER, version), - (UPDATE_SHA256_HEADER, sha), - // A host older than 4.1 knows only the headers' old names, and - // is the one this push is most likely for. - (legacy::UPDATE_VERSION_HEADER, version), - (legacy::UPDATE_SHA256_HEADER, sha), - ("Content-Type", "application/octet-stream"), - ], + &push_headers(version, sha), exe, Duration::from_secs(120), )?; @@ -736,6 +741,19 @@ mod tests { assert!(!msg.contains("os error"), "{msg}"); } + #[test] + fn a_push_carries_both_header_names_so_a_4_0_host_accepts_it() { + let h = push_headers("4.1.1", "abc"); + let get = |name: &str| h.iter().find(|(k, _)| *k == name).map(|(_, v)| *v); + assert_eq!(get("x-latch-version"), Some("4.1.1")); + assert_eq!(get("x-latch-sha256"), Some("abc")); + // What a 4.0 host reads, spelled out here so that renaming the + // constant in legacy.rs cannot silently break the push. + assert_eq!(get("x-brolink-version"), Some("4.1.1")); + assert_eq!(get("x-brolink-sha256"), Some("abc")); + assert_eq!(get("Content-Type"), Some("application/octet-stream")); + } + /// A one-reply HTTP server saying the host runs `version`. fn status_server(version: &'static str) -> SocketAddr { use std::io::{Read, Write}; diff --git a/crates/host/src/update.rs b/crates/host/src/update.rs index 8cffb80..3de9057 100644 --- a/crates/host/src/update.rs +++ b/crates/host/src/update.rs @@ -316,6 +316,28 @@ mod tests { ); } + #[test] + fn a_push_from_a_4_0_mac_is_accepted_under_the_old_header_names() { + let dir = std::env::temp_dir().join(format!("latch-legacy-upd-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let exe = dir.join("brolink-host.exe"); + let body = fake_exe(); + let newer = format!("{}.0.0", update::current().major + 1); + let request = Request { + method: "POST".into(), + path: latch_core::api::UPDATE_PATH.into(), + headers: vec![ + ("x-brolink-version".to_string(), newer.clone()), + ("x-brolink-sha256".to_string(), update::sha256_hex(&body)), + ], + body: body.clone(), + }; + let v = stage(&request, &exe).unwrap(); + assert_eq!(v.to_string(), newer); + assert_eq!(std::fs::read(staged(&exe)).unwrap(), body); + let _ = std::fs::remove_dir_all(&dir); + } + #[test] fn uploads_are_checked_before_anything_is_written() { let dir = std::env::temp_dir().join(format!("latch-upd-{}", std::process::id()));