Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/rust-sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,9 @@ jobs:
BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache
run: |
cargo build
cargo test --features bundled-in-process --lib embedded_archive_contains_only_expected_files
cargo test --features bundled-in-process --lib embeddedcli::tests::
cargo test --features bundled-in-process --test cli_resolution_test install_bundled_
cargo test --features bundled-in-process --test cli_resolution_test bundled_cli_is_distinct_from_runtime_and_supports_version_probe

# Build natively inside Alpine so Cargo reports target_env="musl". This
# exercises selection and extraction of the linuxmusl-arm64 runtime artifact,
Expand Down
7 changes: 7 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ parking_lot = "0.12"
regex = "1"
getrandom = "0.2"
uuid = { version = "1", default-features = false, features = ["v4"] }
flate2 = { version = "1", optional = true }
flate2 = { version = "1", default-features = false, features = ["zlib-rs"], optional = true }
Comment thread
mohamedmansour marked this conversation as resolved.
tar = { version = "0.4", optional = true }
# LLM inference callback transport: idiomatic HTTP/WebSocket forwarding for the
# `CopilotRequestHandler`, plus base64/byte/stream plumbing for the chunk protocol.
Expand All @@ -69,7 +69,8 @@ reqwest = { version = "0.12", default-features = false, features = ["stream", "h
tokio-tungstenite = { version = "0.24", default-features = false, features = ["connect", "native-tls"] }

[target.'cfg(windows)'.dependencies]
zip = { version = "2", default-features = false, features = ["deflate"], optional = true }
# Reuse the runtime flate2 backend instead of enabling zip's miniz_oxide default.
zip = { version = "2", default-features = false, features = ["deflate-flate2", "flate2"], optional = true }
windows-sys = { version = "0.61", default-features = false, features = [
"Win32_Foundation",
"Win32_System_Diagnostics_ToolHelp",
Expand All @@ -78,11 +79,13 @@ windows-sys = { version = "0.61", default-features = false, features = [
] }

[dev-dependencies]
flate2 = { version = "1", default-features = false, features = ["zlib-rs"] }
rusqlite = { version = "0.35", features = ["bundled"] }
schemars = "1"
serial_test = "3"
tempfile = "3"
tokio = { version = "1", features = ["rt-multi-thread"] }
zip = { version = "2", default-features = false, features = ["deflate-flate2", "flate2"] }

# Integration tests that call test-support-only Client methods (e.g.
# `from_streams_with_connection_token`, `from_streams_with_trace_provider`)
Expand Down
139 changes: 135 additions & 4 deletions rust/src/embeddedcli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ fn install_runtime(install_dir: &Path, archive: &[u8]) -> Result<PathBuf, Embedd

#[cfg(has_bundled_cli)]
fn install_hostless_assets(install_dir: &Path, archive: &[u8]) -> Result<(), EmbeddedCliError> {
let gz = flate2::read::GzDecoder::new(archive);
let gz = flate2::bufread::GzDecoder::new(archive);
let mut tar = tar::Archive::new(gz);
for entry in tar
.entries()
Expand Down Expand Up @@ -710,15 +710,19 @@ fn extract_cli_binary(archive: &[u8]) -> Result<Vec<u8>, EmbeddedCliError> {

#[cfg(all(has_bundled_cli, windows))]
fn extract_cli_binary(archive: &[u8]) -> Result<Vec<u8>, EmbeddedCliError> {
extract_zip_binary(archive, CLI_BINARY_NAME)
}

#[cfg(all(has_bundled_cli, any(windows, test)))]
fn extract_zip_binary(archive: &[u8], binary_name: &str) -> Result<Vec<u8>, 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}"))
{
if entry.name() == binary_name || entry.name().ends_with(&format!("/{binary_name}")) {
let mut bytes = Vec::with_capacity(entry.size() as usize);
entry
.read_to_end(&mut bytes)
Expand All @@ -731,7 +735,7 @@ fn extract_cli_binary(archive: &[u8]) -> Result<Vec<u8>, EmbeddedCliError> {

#[cfg(has_bundled_cli)]
fn extract_binary(archive: &[u8], binary_name: &str) -> Result<Vec<u8>, EmbeddedCliError> {
let gz = flate2::read::GzDecoder::new(archive);
let gz = flate2::bufread::GzDecoder::new(archive);
let mut tar = tar::Archive::new(gz);
for entry in tar
.entries()
Expand Down Expand Up @@ -891,6 +895,133 @@ impl std::error::Error for EmbeddedCliError {
mod tests {
use super::*;

#[cfg(has_bundled_cli)]
fn zip_archive(path: &str, bytes: &[u8]) -> Vec<u8> {
let mut archive = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
archive.start_file(path, options).unwrap();
archive.write_all(bytes).unwrap();
archive.finish().unwrap().into_inner()
}

#[cfg(has_bundled_cli)]
#[test]
fn zip_cli_extraction_preserves_bytes_and_entry_selection() {
let bytes: Vec<u8> = (0..=255).cycle().take(65_537).collect();
for name in ["copilot.exe", "package/copilot.exe"] {
let archive = zip_archive(name, &bytes);
assert_eq!(extract_zip_binary(&archive, "copilot.exe").unwrap(), bytes);
}
let archive = zip_archive("not-the-cli", &bytes);
assert!(extract_zip_binary(&archive, "copilot.exe").is_err());
}

#[cfg(has_bundled_cli)]
#[test]
fn zip_cli_extraction_rejects_corrupt_deflate_and_checksum() {
let archive = zip_archive("copilot.exe", &[0xAB; 65_537]);
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&archive)).unwrap();
let payload_start = zip.by_index(0).unwrap().data_start() as usize;
let mut invalid_deflate = archive.clone();
// BTYPE=3 is reserved in DEFLATE, regardless of the chosen encoder.
invalid_deflate[payload_start] |= 0b110;

let central_directory = zip.central_directory_start() as usize;
let mut invalid_crc = archive.clone();
// The central directory file header stores CRC-32 at byte offset 16.
invalid_crc[central_directory + 16] ^= 0xFF;
let truncated = &archive[..archive.len() / 2];

for invalid in [
invalid_deflate.as_slice(),
invalid_crc.as_slice(),
truncated,
] {
let error = extract_zip_binary(invalid, "copilot.exe").unwrap_err();
assert!(
std::error::Error::source(&error).is_some(),
"invalid ZIP must be an archive error, not a missing-file result"
);
}
}

#[cfg(has_bundled_cli)]
fn gzip_archive(path: &str, bytes: &[u8], mode: u32) -> Vec<u8> {
let encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
let mut archive = tar::Builder::new(encoder);
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(mode);
header.set_cksum();
archive.append_data(&mut header, path, bytes).unwrap();
archive.into_inner().unwrap().finish().unwrap()
}

#[cfg(has_bundled_cli)]
#[test]
fn gzip_extraction_preserves_bytes_modes_and_member_boundary() {
let dir = tempfile::tempdir().unwrap();
let bytes: Vec<u8> = (0..=255).cycle().take(65_537).collect();
let mut archive = gzip_archive("assets/first", &bytes, 0o750);
archive.extend(gzip_archive("second", b"another gzip member", 0o644));

assert_eq!(extract_binary(&archive, "first").unwrap(), bytes);
assert!(extract_binary(&archive, "second").is_err());
install_hostless_assets(dir.path(), &archive).unwrap();
let installed = dir.path().join("assets/first");
assert_eq!(fs::read(&installed).unwrap(), bytes);
assert!(!dir.path().join("second").exists());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
fs::metadata(installed).unwrap().permissions().mode() & 0o777,
0o750
);
}
}

#[cfg(has_bundled_cli)]
#[test]
fn gzip_extraction_rejects_invalid_header_and_truncated_payload() {
let archive = gzip_archive("asset", &[0xAB; 65_537], 0o644);
let mut invalid_header = archive.clone();
invalid_header[0] ^= 0xFF;
let truncated = &archive[..archive.len() / 2];
for invalid in [invalid_header.as_slice(), truncated] {
let dir = tempfile::tempdir().unwrap();
assert!(extract_binary(invalid, "asset").is_err());
assert!(install_hostless_assets(dir.path(), invalid).is_err());
assert!(!dir.path().join("asset").exists());
}
}

#[cfg(has_bundled_cli)]
#[test]
fn gzip_extraction_rejects_invalid_trailers_at_end_of_stream() {
// An empty TAR stream reaches the gzip trailer instead of stopping at
// TAR end-of-archive blocks or returning a selected file early.
let empty = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default())
.finish()
.unwrap();
let mut invalid_crc = empty.clone();
invalid_crc[empty.len() - 8] ^= 0xFF;
let mut invalid_size = empty.clone();
invalid_size[empty.len() - 4] ^= 0xFF;
let truncated = &empty[..empty.len() - 1];
let dir = tempfile::tempdir().unwrap();
install_hostless_assets(dir.path(), &empty).unwrap();
for invalid in [invalid_crc.as_slice(), invalid_size.as_slice(), truncated] {
assert!(install_hostless_assets(dir.path(), invalid).is_err());
let error = extract_binary(invalid, "absent").unwrap_err();
assert!(
std::error::Error::source(&error).is_some(),
"invalid gzip must be an archive error, not a missing-file result"
);
}
}

#[cfg(all(has_bundled_cli, feature = "bundled-in-process"))]
#[test]
fn embedded_runtime_archive_contains_runtime_assets_and_excludes_cli() {
Expand Down