//! Lazy runtime installer for the CLI binary that build.rs embedded in this
//! crate (gated on the `bundled-cli` cargo feature, which is in the default
//! feature set).
//!
//! Builds embed two platform release payloads from GitHub Releases: the full
//! CLI archive and a filtered runtime archive containing the wrapper,
//! `runtime.node`, auxiliary runtime assets, and optionally the in-process
//! runtime library. Extraction to a real on-disk path is deferred until the
//! relevant installer is called.
//!
//! The embedded bytes are part of the consumer's signed binary and therefore
//! trusted *as the source of truth* — but the bytes that land on disk are not.
//! A non-atomic write, a multi-process race, or antivirus quarantining the
//! freshly-written executable can leave a truncated or corrupt image that, if
//! handed back as "good", fails to launch (e.g. Windows `ERROR_BAD_EXE_FORMAT`).
//! Installation therefore: extracts to a unique temp file in the target dir,
//! fsyncs and marks it executable, verifies the staged bytes against the
//! trusted in-memory image, atomically renames it into place, re-verifies the
//! published file, and records an integrity marker. Subsequent runs trust an
//! existing install only after a cheap re-check (size marker + executable-image
//! header); anything that looks truncated or quarantined is re-extracted, and
//! the whole publish is retried before surfacing a clear, actionable error.
// The atomic-publish + verify helpers (and their unit tests) are pure
// std-only logic that doesn't touch the embedded archive, so they compile
// whenever the binary is bundled *or* we're building the test harness —
// the standard `cargo test --no-default-features` job has `has_bundled_cli`
// off but still needs to exercise them.
#[cfg(any(has_bundled_cli, test))]
use std::fs;
#[cfg(has_bundled_cli)]
use std::io::Read;
#[cfg(any(has_bundled_cli, test))]
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
#[cfg(any(has_bundled_cli, test))]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(has_bundled_cli)]
use tracing::{info, warn};
// When the `bundled-cli` cargo feature is enabled and the target platform is
// supported, build.rs generates `bundled_cli.rs` exposing both selected archives.
// The CLI version is exposed crate-wide via the
// `cargo:rustc-env=COPILOT_SDK_CLI_VERSION` emit (see `build.rs`), and the
// binary name is OS-derived — so no other generated constants are needed.
#[cfg(has_bundled_cli)]
mod build_time {
include!(concat!(env!("OUT_DIR"), "/bundled_cli.rs"));
}
// Pinned at build time and consumed by both install paths (path/install_at).
// Sourced from the unconditional `COPILOT_SDK_CLI_VERSION` env emit in
// build.rs — the single source of truth for "what version did build.rs
// target", shared with the runtime resolver used when `bundled-cli` is off.
#[cfg(has_bundled_cli)]
const CLI_VERSION: &str = env!("COPILOT_SDK_CLI_VERSION");
// OS-derived; matches the release-archive entry name and the on-disk
// filename. No need to bake this — `cfg(windows)` reflects the target
// the runtime is running on, which by definition is the same target
// build.rs targeted.
#[cfg(all(has_bundled_cli, windows))]
const CLI_BINARY_NAME: &str = "copilot.exe";
#[cfg(all(has_bundled_cli, not(windows)))]
const CLI_BINARY_NAME: &str = "copilot";
#[cfg(all(has_bundled_cli, windows))]
const RUNTIME_BINARY_NAME: &str = "copilot-runtime.exe";
#[cfg(all(has_bundled_cli, not(windows)))]
const RUNTIME_BINARY_NAME: &str = "copilot-runtime";
#[cfg(has_bundled_cli)]
const RUNTIME_NODE_NAME: &str = "runtime.node";
#[cfg(has_bundled_cli)]
const RUNTIME_VERSION_MARKER: &str = ".copilot-runtime-version";
#[cfg(feature = "bundled-cli")]
static INSTALLED_PATH: OnceLock> = OnceLock::new();
#[cfg(feature = "bundled-cli")]
static INSTALLED_RUNTIME_PATH: OnceLock > = OnceLock::new();
/// Returns the path to the installed CLI binary, lazily extracting the
/// embedded archive on first call.
///
/// On first call this extracts the embedded archive to
/// `/github-copilot-sdk/cli//copilot[.exe]`
/// and returns the resulting path. The cache dir comes from
/// [`dirs::cache_dir()`] — `%LOCALAPPDATA%` on Windows,
/// `~/Library/Caches/` on macOS, `$XDG_CACHE_HOME` (or `~/.cache/`) on
/// Linux. Subsequent calls return the cached result. Extraction
/// is skipped when a previously-published binary is still present and
/// passes a cheap integrity re-check (size marker + executable-image
/// header); a truncated, empty, or quarantined binary is re-extracted
/// rather than returned.
///
/// Returns `None` if no CLI was embedded at build time.
#[cfg(feature = "bundled-cli")]
pub(crate) fn path() -> Option {
INSTALLED_PATH
.get_or_init(|| {
#[cfg(has_bundled_cli)]
{
let dir = default_install_dir(CLI_VERSION);
match install_cli(
&dir,
build_time::CLI_ARCHIVE,
build_time::CLI_BINARY_SIZE,
) {
Ok(path) => {
info!(path = %path.display(), version = CLI_VERSION, "embedded CLI installed");
return Some(path);
}
Err(e) => {
warn!(error = %e, "embedded CLI installation failed");
}
}
}
None
})
.clone()
}
/// Install the embedded CLI binary into the given directory instead of the
/// default `/github-copilot-sdk/cli//` location
/// (see [`path`] for the per-platform mapping).
///
/// Idempotent: skips extraction when an already-published binary passes the
/// integrity re-check (size marker + executable-image header), and
/// re-extracts a corrupt or quarantined one.
/// Returns `None` when the SDK was built without a bundled CLI.
#[cfg(feature = "bundled-cli")]
#[allow(dead_code)] // Used by resolve.rs when ClientOptions::bundled_cli_extract_dir is set.
pub(crate) fn install_at(extract_dir: &Path) -> Option {
#[cfg(has_bundled_cli)]
{
match install_cli(
extract_dir,
build_time::CLI_ARCHIVE,
build_time::CLI_BINARY_SIZE,
) {
Ok(path) => {
info!(path = %path.display(), version = CLI_VERSION, "embedded CLI installed");
return Some(path);
}
Err(e) => {
warn!(error = %e, "embedded CLI installation failed");
}
}
}
#[cfg(not(has_bundled_cli))]
{
let _ = extract_dir;
}
None
}
/// Returns the path to the bundled runtime wrapper, extracting the wrapper and
/// adjacent `runtime.node` on first call.
#[cfg(feature = "bundled-cli")]
pub(crate) fn runtime_path() -> Option {
INSTALLED_RUNTIME_PATH
.get_or_init(|| {
#[cfg(has_bundled_cli)]
{
let dir = default_install_dir(CLI_VERSION);
match install_runtime(&dir, build_time::RUNTIME_ARCHIVE) {
Ok(path) => {
info!(path = %path.display(), version = CLI_VERSION, "embedded runtime installed");
return Some(path);
}
Err(e) => {
warn!(error = %e, "embedded runtime installation failed");
}
}
}
None
})
.clone()
}
/// Installs the bundled runtime wrapper and adjacent `runtime.node` into a
/// caller-specified directory.
#[cfg(feature = "bundled-cli")]
pub(crate) fn install_runtime_at(extract_dir: &Path) -> Option {
#[cfg(has_bundled_cli)]
{
let install_dir = match runtime_install_dir(extract_dir, CLI_VERSION) {
Ok(dir) => dir,
Err(e) => {
warn!(error = %e, "embedded runtime install directory selection failed");
return None;
}
};
match install_runtime(&install_dir, build_time::RUNTIME_ARCHIVE) {
Ok(path) => {
info!(path = %path.display(), version = CLI_VERSION, "embedded runtime installed");
return Some(path);
}
Err(e) => {
warn!(error = %e, "embedded runtime installation failed");
}
}
}
#[cfg(not(has_bundled_cli))]
{
let _ = extract_dir;
}
None
}
#[cfg(has_bundled_cli)]
fn runtime_install_dir(base_dir: &Path, version: &str) -> Result {
fs::create_dir_all(base_dir)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e))?;
let marker = base_dir.join(RUNTIME_VERSION_MARKER);
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&marker)
{
Ok(mut file) => {
if let Err(error) = file
.write_all(version.as_bytes())
.and_then(|()| file.sync_all())
{
drop(file);
let _ = fs::remove_file(&marker);
return Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Io, error));
}
Ok(base_dir.to_path_buf())
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
let installed_version = fs::read_to_string(marker).unwrap_or_default();
if installed_version == version {
Ok(base_dir.to_path_buf())
} else {
Ok(base_dir.join(version))
}
}
Err(error) => Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Io, error)),
}
}
#[cfg(has_bundled_cli)]
fn default_install_dir(version: &str) -> PathBuf {
let cache = dirs::cache_dir().unwrap_or_else(std::env::temp_dir);
let root = cache.join("github-copilot-sdk").join("cli");
if version.is_empty() {
root.join("unversioned")
} else {
root.join(sanitize_version(version))
}
}
/// Number of times we re-extract + re-publish the binary before giving up.
/// A single transient failure (e.g. antivirus briefly locking or quarantining
/// the freshly-written file) is retried; a persistent one surfaces a clear
/// error rather than handing back a broken path.
#[cfg(has_bundled_cli)]
const MAX_PUBLISH_ATTEMPTS: u32 = 3;
// Natural platform shared-library name for the in-process FFI runtime.
#[cfg(all(has_bundled_cli, feature = "bundled-in-process", windows))]
const RUNTIME_LIBRARY_NAME: &str = "copilot_runtime.dll";
#[cfg(all(has_bundled_cli, feature = "bundled-in-process", target_os = "macos"))]
const RUNTIME_LIBRARY_NAME: &str = "libcopilot_runtime.dylib";
#[cfg(all(
has_bundled_cli,
feature = "bundled-in-process",
not(windows),
not(target_os = "macos")
))]
const RUNTIME_LIBRARY_NAME: &str = "libcopilot_runtime.so";
#[cfg(has_bundled_cli)]
fn install_runtime(install_dir: &Path, archive: &[u8]) -> Result {
fs::create_dir_all(install_dir)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e))?;
install_hostless_assets(install_dir, archive)?;
install_runtime_pair(install_dir, archive)?;
#[cfg(feature = "bundled-in-process")]
install_runtime_library(install_dir, archive)?;
Ok(install_dir.join(RUNTIME_BINARY_NAME))
}
#[cfg(has_bundled_cli)]
fn install_hostless_assets(install_dir: &Path, archive: &[u8]) -> Result<(), EmbeddedCliError> {
let gz = flate2::read::GzDecoder::new(archive);
let mut tar = tar::Archive::new(gz);
for entry in tar
.entries()
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?
{
let mut entry =
entry.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
if !entry.header().entry_type().is_file() {
continue;
}
let path = entry
.path()
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?
.into_owned();
let file_name = path.file_name().and_then(|name| name.to_str());
if path == Path::new(CLI_BINARY_NAME)
|| matches!(
file_name,
Some("copilot_runtime.dll")
| Some("libcopilot_runtime.dylib")
| Some("libcopilot_runtime.so")
)
{
continue;
}
if path.is_absolute()
|| path.components().any(|component| {
matches!(
component,
std::path::Component::Prefix(_)
| std::path::Component::RootDir
| std::path::Component::ParentDir
)
})
{
return Err(EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Archive,
format!("unsafe embedded runtime asset path: {}", path.display()),
));
}
let mut bytes = Vec::with_capacity(entry.size() as usize);
entry
.read_to_end(&mut bytes)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
let target = install_dir.join(&path);
if fs::read(&target)
.map(|installed| installed == bytes)
.unwrap_or(false)
{
continue;
}
let parent = target.parent().ok_or_else(|| {
EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Archive,
format!("embedded runtime asset has no parent: {}", path.display()),
)
})?;
fs::create_dir_all(parent)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e))?;
let tmp = write_temp_file(parent, &bytes)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = entry.header().mode().unwrap_or(0o644) & 0o777;
fs::set_permissions(&tmp, fs::Permissions::from_mode(mode))
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))?;
}
if let Err(error) = publish(&tmp, &target) {
let _ = fs::remove_file(&tmp);
return Err(error);
}
}
Ok(())
}
#[cfg(has_bundled_cli)]
fn install_runtime_pair(install_dir: &Path, archive: &[u8]) -> Result<(), EmbeddedCliError> {
install_adjacent_file(install_dir, archive, RUNTIME_NODE_NAME, "runtime.node")?;
install_adjacent_file(
install_dir,
archive,
RUNTIME_BINARY_NAME,
"copilot runtime wrapper",
)
}
#[cfg(all(has_bundled_cli, feature = "bundled-in-process"))]
fn install_runtime_library(install_dir: &Path, archive: &[u8]) -> Result<(), EmbeddedCliError> {
install_adjacent_file(
install_dir,
archive,
RUNTIME_LIBRARY_NAME,
"in-process FFI runtime library",
)
}
#[cfg(has_bundled_cli)]
fn install_adjacent_file(
install_dir: &Path,
archive: &[u8],
file_name: &str,
label: &str,
) -> Result<(), EmbeddedCliError> {
let target = install_dir.join(file_name);
let bytes = extract_binary(archive, file_name)?;
if bytes.is_empty() {
return Err(EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Verification,
format!("embedded {label} is empty"),
));
}
if fs::read(&target)
.map(|installed| installed == bytes)
.unwrap_or(false)
{
return Ok(());
}
let tmp = write_temp_file(install_dir, &bytes)?;
if let Err(e) = publish(&tmp, &target) {
let _ = fs::remove_file(&tmp);
return Err(e);
}
tracing::debug!(path = %target.display(), %label, "embedded runtime artifact installed");
Ok(())
}
#[cfg(has_bundled_cli)]
fn install_cli(
install_dir: &Path,
archive: &[u8],
expected_binary_size: u64,
) -> Result {
let verbose = std::env::var("COPILOT_CLI_INSTALL_VERBOSE").ok().as_deref() == Some("1");
fs::create_dir_all(install_dir)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e))?;
let final_path = install_dir.join(CLI_BINARY_NAME);
let marker_path = marker_path(install_dir);
// Fast path: a previous install left both the binary and the integrity
// marker we wrote *after* verifying it. Re-validate cheaply (size +
// executable-image magic) so a binary that was later truncated or
// quarantined by antivirus is re-extracted instead of trusted blindly.
if existing_install_is_valid(&final_path, &marker_path, expected_binary_size) {
if verbose {
eprintln!("embedded CLI already installed at {}", final_path.display());
}
return Ok(final_path);
}
// The bytes extracted from the embedded archive are part of the
// consumer's trusted, signed binary — so they are the known-good
// reference we verify the on-disk file against after publishing.
let start = std::time::Instant::now();
let bytes = extract_cli_binary(archive)?;
if bytes.is_empty() {
return Err(EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Verification,
"extracted CLI binary is empty",
));
}
let mut last_err: Option = None;
for attempt in 1..=MAX_PUBLISH_ATTEMPTS {
match publish_verified(install_dir, &final_path, &marker_path, &bytes) {
Ok(()) => {
if verbose {
eprintln!(
"embedded CLI extracted to {} in {:?}",
final_path.display(),
start.elapsed()
);
}
return Ok(final_path);
}
Err(e) => {
// Another process may have raced us and published the same
// good binary; if what's on disk matches our trusted bytes,
// accept its install rather than fighting over it.
if verify_on_disk_matches(&final_path, &bytes).is_ok() {
let _ = write_marker(&marker_path, bytes.len() as u64);
return Ok(final_path);
}
warn!(attempt, error = %e, "embedded CLI publish attempt failed; retrying");
last_err = Some(e);
}
}
}
Err(EmbeddedCliError::with_source(
EmbeddedCliErrorKind::Blocked,
last_err,
))
}
/// Path of the integrity marker written next to the installed binary. Its
/// presence (and recorded size) is proof a previous run published a verified
/// binary, letting the fast path skip re-extraction without trusting a bare
/// `is_file()` check.
#[cfg(any(has_bundled_cli, test))]
fn marker_path(install_dir: &Path) -> PathBuf {
install_dir.join(".copilot-cli.ok")
}
/// Cheap, allocation-light validity check for an already-installed binary:
/// the file exists and is non-empty, an integrity marker recording its
/// expected size is present and matches, and the first bytes look like a
/// valid executable image for this platform. Catches the realistic failure
/// modes (zero-length / truncated / quarantined-to-garbage) without re-reading
/// the whole file.
#[cfg(any(has_bundled_cli, test))]
fn existing_install_is_valid(
final_path: &Path,
marker_path: &Path,
expected_binary_size: u64,
) -> bool {
let Ok(meta) = fs::metadata(final_path) else {
return false;
};
if !meta.is_file() || meta.len() == 0 {
return false;
}
match read_marker_len(marker_path) {
Some(expected) if expected == expected_binary_size && expected == meta.len() => {
looks_like_valid_image(final_path)
}
_ => false,
}
}
/// Extract → stage in a unique temp file in the *same* directory → verify the
/// staged bytes → atomically rename into place → re-verify the published file
/// → write the integrity marker. Every step that can leave a partial file
/// cleans up after itself, so a failure never leaves a half-written binary at
/// the final path.
#[cfg(any(has_bundled_cli, test))]
fn publish_verified(
install_dir: &Path,
final_path: &Path,
marker_path: &Path,
bytes: &[u8],
) -> Result<(), EmbeddedCliError> {
let tmp = write_temp_file(install_dir, bytes)?;
// Verify the staged copy before it ever becomes the live binary, so a
// short write or in-flight antivirus tampering is caught here.
if let Err(e) = verify_on_disk_matches(&tmp, bytes) {
let _ = fs::remove_file(&tmp);
return Err(e);
}
if let Err(e) = publish(&tmp, final_path) {
let _ = fs::remove_file(&tmp);
return Err(e);
}
// Re-verify after the rename: catches the window where antivirus
// quarantines or rewrites the file between staging and publishing.
verify_on_disk_matches(final_path, bytes)?;
write_marker(marker_path, bytes.len() as u64)?;
Ok(())
}
/// Write `contents` to a uniquely-named temp file in `dir` (same filesystem as
/// the final path so the later rename is atomic), flushing and fsync-ing the
/// bytes to disk and marking it executable on unix before returning its path.
#[cfg(any(has_bundled_cli, test))]
fn write_temp_file(dir: &Path, contents: &[u8]) -> Result {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let unique = format!(
".copilot-cli.tmp.{}.{}.{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed),
nanos
);
let tmp = dir.join(unique);
// `create_new` guarantees we never clobber a sibling's in-flight temp
// file (the pid + counter + nanos name already makes that practically
// impossible).
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))?;
if let Err(e) = file
.write_all(contents)
.and_then(|()| file.flush())
.and_then(|()| file.sync_all())
{
drop(file);
let _ = fs::remove_file(&tmp);
return Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Err(e) = fs::set_permissions(&tmp, fs::Permissions::from_mode(0o755)) {
drop(file);
let _ = fs::remove_file(&tmp);
return Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e));
}
}
drop(file);
Ok(tmp)
}
/// Atomically move the staged temp file onto `final_path`.
///
/// `rename` replaces the target atomically on POSIX, but on Windows it fails
/// when the target already exists — so on that error we remove the stale file
/// and retry. The remove-then-rename is the only non-atomic window, and it's
/// guarded upstream: callers re-verify the published file and, on a lost race,
/// accept a peer's identical install instead of erroring.
#[cfg(any(has_bundled_cli, test))]
fn publish(tmp: &Path, final_path: &Path) -> Result<(), EmbeddedCliError> {
match fs::rename(tmp, final_path) {
Ok(()) => Ok(()),
Err(_) if final_path.exists() => {
let _ = fs::remove_file(final_path);
fs::rename(tmp, final_path)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Publish, e))
}
Err(e) => Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Publish, e)),
}
}
/// Read the file at `path` and confirm it byte-for-byte matches the trusted
/// `expected` image. Size is checked first so the common corruption case
/// (truncation) produces a precise error.
#[cfg(any(has_bundled_cli, test))]
fn verify_on_disk_matches(path: &Path, expected: &[u8]) -> Result<(), EmbeddedCliError> {
let actual = fs::read(path).map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))?;
if actual.len() != expected.len() {
return Err(EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Verification,
format!(
"size mismatch: on-disk {} bytes, expected {} bytes",
actual.len(),
expected.len()
),
));
}
if actual != expected {
return Err(EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Verification,
"on-disk binary differs from the embedded image",
));
}
Ok(())
}
/// Best-effort check that the first bytes of `path` are a valid executable
/// image header for the current platform (PE on Windows, Mach-O on macOS,
/// ELF elsewhere). Returns `false` on any I/O error or unrecognized header.
#[cfg(any(has_bundled_cli, test))]
fn looks_like_valid_image(path: &Path) -> bool {
use std::io::Read as _;
let mut buf = [0u8; 4];
let Ok(mut file) = fs::File::open(path) else {
return false;
};
let Ok(read) = file.read(&mut buf) else {
return false;
};
let head = &buf[..read];
#[cfg(windows)]
{
head.starts_with(b"MZ")
}
#[cfg(target_os = "macos")]
{
matches!(
head,
[0xfe, 0xed, 0xfa, 0xce] // Mach-O 32-bit
| [0xfe, 0xed, 0xfa, 0xcf] // Mach-O 64-bit
| [0xce, 0xfa, 0xed, 0xfe] // byte-swapped 32-bit
| [0xcf, 0xfa, 0xed, 0xfe] // byte-swapped 64-bit
| [0xca, 0xfe, 0xba, 0xbe] // universal (fat)
| [0xbe, 0xba, 0xfe, 0xca] // byte-swapped universal
)
}
#[cfg(all(not(windows), not(target_os = "macos")))]
{
head.starts_with(b"\x7fELF")
}
}
/// Write the integrity marker recording the published binary's size. Best
/// effort: a torn write just means the next run can't parse it and re-extracts.
#[cfg(any(has_bundled_cli, test))]
fn write_marker(marker_path: &Path, size: u64) -> Result<(), EmbeddedCliError> {
fs::write(marker_path, size.to_string())
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))
}
/// Parse the size recorded in the integrity marker, or `None` if it's missing
/// or unparsable.
#[cfg(any(has_bundled_cli, test))]
fn read_marker_len(marker_path: &Path) -> Option {
fs::read_to_string(marker_path)
.ok()?
.trim()
.parse::()
.ok()
}
#[cfg(all(has_bundled_cli, not(windows)))]
fn extract_cli_binary(archive: &[u8]) -> Result, EmbeddedCliError> {
extract_binary(archive, CLI_BINARY_NAME)
}
#[cfg(all(has_bundled_cli, windows))]
fn extract_cli_binary(archive: &[u8]) -> Result, EmbeddedCliError> {
let reader = std::io::Cursor::new(archive);
let mut zip = zip::ZipArchive::new(reader)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
for index in 0..zip.len() {
let mut entry = zip
.by_index(index)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
if entry.name() == CLI_BINARY_NAME || entry.name().ends_with(&format!("/{CLI_BINARY_NAME}"))
{
let mut bytes = Vec::with_capacity(entry.size() as usize);
entry
.read_to_end(&mut bytes)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
return Ok(bytes);
}
}
Err(EmbeddedCliErrorKind::BinaryNotFoundInArchive.into())
}
#[cfg(has_bundled_cli)]
fn extract_binary(archive: &[u8], binary_name: &str) -> Result, EmbeddedCliError> {
let gz = flate2::read::GzDecoder::new(archive);
let mut tar = tar::Archive::new(gz);
for entry in tar
.entries()
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?
{
let mut entry =
entry.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
let path = entry
.path()
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
let name = path.to_string_lossy();
if name == binary_name || name.ends_with(&format!("/{binary_name}")) {
let mut bytes = Vec::with_capacity(entry.size() as usize);
entry
.read_to_end(&mut bytes)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
return Ok(bytes);
}
}
Err(EmbeddedCliErrorKind::BinaryNotFoundInArchive.into())
}
#[cfg(has_bundled_cli)]
fn sanitize_version(version: &str) -> String {
version
.chars()
.map(|c| match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '-' | '_' => c,
_ => '_',
})
.collect()
}
#[cfg(any(has_bundled_cli, test))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(dead_code)]
enum EmbeddedCliErrorKind {
CreateDir,
Archive,
BinaryNotFoundInArchive,
Io,
/// Atomically renaming the staged temp file onto the final path failed.
Publish,
/// The published (or staged) file didn't match the trusted embedded image.
Verification,
/// Extraction kept producing a corrupt/missing binary across all retries —
/// most likely antivirus interference.
Blocked,
}
#[cfg(any(has_bundled_cli, test))]
impl std::fmt::Display for EmbeddedCliErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EmbeddedCliErrorKind::CreateDir => f.write_str("failed to create install directory"),
EmbeddedCliErrorKind::Archive => f.write_str("failed to read archive entry"),
EmbeddedCliErrorKind::BinaryNotFoundInArchive => {
f.write_str("CLI binary not found in embedded archive")
}
EmbeddedCliErrorKind::Io => f.write_str("I/O error"),
EmbeddedCliErrorKind::Publish => {
f.write_str("failed to publish the extracted CLI binary")
}
EmbeddedCliErrorKind::Verification => {
f.write_str("extracted CLI binary failed integrity verification")
}
EmbeddedCliErrorKind::Blocked => f.write_str(
"bundled CLI appears blocked or corrupt after multiple attempts \
(possibly quarantined by antivirus)",
),
}
}
}
#[cfg(any(has_bundled_cli, test))]
#[allow(dead_code)]
struct EmbeddedCliError {
repr: crate::errors::Repr,
}
#[cfg(any(has_bundled_cli, test))]
#[allow(dead_code)]
impl EmbeddedCliError {
fn new(kind: EmbeddedCliErrorKind, error: E) -> Self
where
E: Into>,
{
Self {
repr: crate::errors::Repr::Custom(crate::errors::Custom {
kind,
error: error.into(),
}),
}
}
fn with_message(
kind: EmbeddedCliErrorKind,
message: impl Into>,
) -> Self {
Self {
repr: crate::errors::Repr::SimpleMessage(kind, message.into()),
}
}
/// Build an error from `kind`, attaching the last failure as the source
/// when one is available so the actionable message still carries context.
fn with_source(kind: EmbeddedCliErrorKind, source: Option) -> Self {
match source {
Some(source) => Self::new(kind, Box::new(source)),
None => Self {
repr: crate::errors::Repr::Simple(kind),
},
}
}
}
#[cfg(any(has_bundled_cli, test))]
impl From for EmbeddedCliError {
fn from(kind: EmbeddedCliErrorKind) -> Self {
Self {
repr: crate::errors::Repr::Simple(kind),
}
}
}
#[cfg(any(has_bundled_cli, test))]
impl std::fmt::Display for EmbeddedCliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.repr {
crate::errors::Repr::Simple(kind) => write!(f, "{kind}"),
crate::errors::Repr::SimpleMessage(_, msg) => write!(f, "{msg}"),
crate::errors::Repr::Custom(crate::errors::Custom { kind, error }) => {
write!(f, "{kind}: {error}")
}
}
}
}
#[cfg(any(has_bundled_cli, test))]
impl std::fmt::Debug for EmbeddedCliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "EmbeddedCliError({self})")
}
}
#[cfg(any(has_bundled_cli, test))]
impl std::error::Error for EmbeddedCliError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.repr {
crate::errors::Repr::Custom(crate::errors::Custom { error, .. }) => Some(&**error),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(all(has_bundled_cli, feature = "bundled-in-process"))]
#[test]
fn embedded_runtime_archive_contains_runtime_assets_and_excludes_cli() {
let gz = flate2::read::GzDecoder::new(build_time::RUNTIME_ARCHIVE);
let mut archive = tar::Archive::new(gz);
let mut names: Vec = archive
.entries()
.expect("archive entries")
.map(|entry| {
entry
.expect("archive entry")
.path()
.expect("archive path")
.to_string_lossy()
.into_owned()
})
.collect();
names.sort();
assert!(names.contains(&RUNTIME_LIBRARY_NAME.to_string()));
assert!(names.contains(&RUNTIME_BINARY_NAME.to_string()));
assert!(names.contains(&RUNTIME_NODE_NAME.to_string()));
assert!(names.iter().any(|name| name.starts_with("ripgrep/")));
assert!(names.iter().any(|name| name.starts_with("definitions/")));
assert!(!names.contains(&CLI_BINARY_NAME.to_string()));
assert!(!names.contains(&"app.js".to_string()));
}
/// Bytes whose header looks like a valid executable image on the host
/// platform, so `looks_like_valid_image` accepts them. `extra` padding
/// bytes follow the magic so size checks have something to disagree about.
fn fake_image(extra: usize) -> Vec {
let mut bytes = Vec::new();
#[cfg(windows)]
bytes.extend_from_slice(b"MZ\x90\x00");
#[cfg(target_os = "macos")]
bytes.extend_from_slice(&[0xfe, 0xed, 0xfa, 0xcf]);
#[cfg(all(not(windows), not(target_os = "macos")))]
bytes.extend_from_slice(b"\x7fELF");
bytes.extend(std::iter::repeat_n(0xAB, extra));
bytes
}
#[test]
fn publish_verified_writes_and_records_marker() {
let dir = tempfile::tempdir().expect("tempdir");
let final_path = dir.path().join("copilot-bin");
let marker = marker_path(dir.path());
let bytes = fake_image(2048);
publish_verified(dir.path(), &final_path, &marker, &bytes).expect("publish");
assert!(final_path.is_file(), "binary should be published");
assert_eq!(fs::read(&final_path).expect("read"), bytes);
assert_eq!(read_marker_len(&marker), Some(bytes.len() as u64));
assert!(existing_install_is_valid(
&final_path,
&marker,
bytes.len() as u64
));
// No leftover temp files in the install dir.
let leftovers: Vec<_> = fs::read_dir(dir.path())
.expect("read_dir")
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
.collect();
assert!(leftovers.is_empty(), "temp files should be cleaned up");
}
#[test]
fn publish_overwrites_an_existing_binary() {
let dir = tempfile::tempdir().expect("tempdir");
let final_path = dir.path().join("copilot-bin");
let marker = marker_path(dir.path());
// Pre-existing (stale) binary at the destination.
fs::write(&final_path, b"old contents").expect("seed");
let bytes = fake_image(512);
publish_verified(dir.path(), &final_path, &marker, &bytes).expect("publish");
assert_eq!(fs::read(&final_path).expect("read"), bytes);
}
#[test]
fn corrupt_or_unmarked_install_is_rejected() {
let dir = tempfile::tempdir().expect("tempdir");
let final_path = dir.path().join("copilot-bin");
let marker = marker_path(dir.path());
let bytes = fake_image(4096);
// Missing binary entirely.
assert!(!existing_install_is_valid(&final_path, &marker, 1));
// Valid binary but no marker (e.g. installed by an older SDK).
fs::write(&final_path, &bytes).expect("write binary");
assert!(
!existing_install_is_valid(&final_path, &marker, bytes.len() as u64),
"an install without a marker must not be trusted"
);
// Marker present but the binary was later truncated (partial write /
// antivirus). Marker still records the original full size.
write_marker(&marker, bytes.len() as u64).expect("marker");
assert!(existing_install_is_valid(
&final_path,
&marker,
bytes.len() as u64
));
assert!(
!existing_install_is_valid(&final_path, &marker, bytes.len() as u64 + 1),
"a marker from the wrapper-as-CLI regression must not validate the full CLI"
);
fs::write(&final_path, &bytes[..bytes.len() / 2]).expect("truncate");
assert!(
!existing_install_is_valid(&final_path, &marker, bytes.len() as u64),
"a truncated binary must be detected via the size marker"
);
// Zero-length binary (quarantined to empty).
fs::write(&final_path, b"").expect("empty");
assert!(!existing_install_is_valid(
&final_path,
&marker,
bytes.len() as u64
));
}
#[test]
fn invalid_image_header_is_rejected() {
let dir = tempfile::tempdir().expect("tempdir");
let final_path = dir.path().join("copilot-bin");
let marker = marker_path(dir.path());
// Right size, has a marker, but the bytes are not a valid image.
let garbage = vec![0u8; 4096];
fs::write(&final_path, &garbage).expect("write garbage");
write_marker(&marker, garbage.len() as u64).expect("marker");
assert!(
!existing_install_is_valid(&final_path, &marker, garbage.len() as u64),
"a non-executable image must be rejected even with a matching marker"
);
}
#[test]
fn verification_rejects_size_and_content_mismatch() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("staged");
let expected = fake_image(1024);
// Exact match passes.
fs::write(&path, &expected).expect("write");
verify_on_disk_matches(&path, &expected).expect("exact match should verify");
// Truncated -> size mismatch.
fs::write(&path, &expected[..100]).expect("truncate");
assert!(verify_on_disk_matches(&path, &expected).is_err());
// Same length, different bytes -> content mismatch.
let mut tampered = expected.clone();
*tampered.last_mut().expect("non-empty") ^= 0xFF;
fs::write(&path, &tampered).expect("tamper");
assert!(verify_on_disk_matches(&path, &expected).is_err());
// Missing file -> I/O error.
fs::remove_file(&path).expect("remove");
assert!(verify_on_disk_matches(&path, &expected).is_err());
}
#[test]
fn temp_files_are_unique_and_synced() {
let dir = tempfile::tempdir().expect("tempdir");
let data = fake_image(256);
let a = write_temp_file(dir.path(), &data).expect("temp a");
let b = write_temp_file(dir.path(), &data).expect("temp b");
assert_ne!(a, b, "temp file names must be unique");
assert_eq!(fs::read(&a).expect("read a"), data);
assert_eq!(fs::read(&b).expect("read b"), data);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = fs::metadata(&a).expect("meta").permissions().mode();
assert_eq!(mode & 0o777, 0o755, "temp binary should be executable");
}
}
#[cfg(has_bundled_cli)]
#[test]
fn runtime_install_replaces_stale_pair() {
let dir = tempfile::tempdir().expect("tempdir");
fs::write(dir.path().join(RUNTIME_NODE_NAME), b"stale runtime").expect("seed runtime");
fs::write(dir.path().join(RUNTIME_BINARY_NAME), b"stale wrapper").expect("seed wrapper");
install_runtime(dir.path(), build_time::RUNTIME_ARCHIVE).expect("install runtime");
assert_eq!(
fs::read(dir.path().join(RUNTIME_NODE_NAME)).expect("read runtime"),
extract_binary(build_time::RUNTIME_ARCHIVE, RUNTIME_NODE_NAME)
.expect("extract runtime")
);
assert_eq!(
fs::read(dir.path().join(RUNTIME_BINARY_NAME)).expect("read wrapper"),
extract_binary(build_time::RUNTIME_ARCHIVE, RUNTIME_BINARY_NAME)
.expect("extract wrapper")
);
}
#[cfg(has_bundled_cli)]
#[test]
fn custom_runtime_install_dir_isolated_by_version() {
let dir = tempfile::tempdir().expect("tempdir");
assert_eq!(
runtime_install_dir(dir.path(), "1.0.0").expect("claim directory"),
dir.path()
);
assert_eq!(
runtime_install_dir(dir.path(), "1.0.0").expect("reuse directory"),
dir.path()
);
assert_eq!(
runtime_install_dir(dir.path(), "2.0.0").expect("isolate directory"),
dir.path().join("2.0.0")
);
}
}