vtcode 0.141.5

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use std::path::{Path, PathBuf};

use anyhow::{Context, Result, bail};
use std::fs::{self, File};
use std::io;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ArchiveKind {
    TarGz,
    Zip,
}

pub(super) fn extract_binary(
    archive_path: &Path,
    archive_kind: ArchiveKind,
    destination: &Path,
    binary_name: &str,
) -> Result<PathBuf> {
    fs::create_dir_all(destination)
        .with_context(|| format!("failed to create extraction directory {}", destination.display()))?;
    let output_path = destination.join(format!(".{binary_name}.staged"));
    let mut matched = false;

    match archive_kind {
        ArchiveKind::TarGz => {
            let archive = File::open(archive_path)
                .with_context(|| format!("failed to open tar-gzip archive {}", archive_path.display()))?;
            let decoder = flate2::read::GzDecoder::new(archive);
            let mut archive = tar::Archive::new(decoder);
            for entry in archive.entries().context("failed to read tar-gzip archive entries")? {
                let mut entry = entry.context("failed to read tar-gzip archive entry")?;
                let path = safe_archive_path(&entry.path().context("failed to read tar archive path")?)?;
                let entry_type = entry.header().entry_type();
                if entry_type.is_symlink() || entry_type.is_hard_link() {
                    bail!("unsafe archive link: {}", path.display());
                }
                if !entry_type.is_file() || path.file_name().and_then(|name| name.to_str()) != Some(binary_name) {
                    continue;
                }
                if matched {
                    bail!("archive contains multiple {binary_name} binaries");
                }
                let mut output =
                    File::create(&output_path).with_context(|| format!("failed to stage {binary_name}"))?;
                io::copy(&mut entry, &mut output).context("failed to extract VT Code binary")?;
                set_executable_permissions(&output_path, entry.header().mode().unwrap_or(0o755))?;
                matched = true;
            }
        }
        ArchiveKind::Zip => {
            let archive = File::open(archive_path)
                .with_context(|| format!("failed to open zip archive {}", archive_path.display()))?;
            let mut archive = zip::ZipArchive::new(archive).context("failed to read zip archive")?;
            for index in 0..archive.len() {
                let mut entry = archive.by_index(index).context("failed to read zip archive entry")?;
                let path = entry
                    .enclosed_name()
                    .context("unsafe archive path: zip entry escapes extraction directory")?;
                let path = safe_archive_path(&path)?;
                if is_zip_symlink(&entry) {
                    bail!("unsafe archive link: {}", path.display());
                }
                if entry.is_dir() || path.file_name().and_then(|name| name.to_str()) != Some(binary_name) {
                    continue;
                }
                if matched {
                    bail!("archive contains multiple {binary_name} binaries");
                }
                let mut output =
                    File::create(&output_path).with_context(|| format!("failed to stage {binary_name}"))?;
                io::copy(&mut entry, &mut output).context("failed to extract VT Code binary")?;
                set_executable_permissions(&output_path, zip_mode(&entry).unwrap_or(0o755))?;
                matched = true;
            }
        }
    }

    if !matched {
        bail!("could not find {binary_name} in update archive");
    }
    Ok(output_path)
}

fn safe_archive_path(path: &Path) -> Result<PathBuf> {
    if path.is_absolute() {
        bail!("unsafe archive path: {}", path.display());
    }

    let mut safe_path = PathBuf::new();
    for component in path.components() {
        match component {
            std::path::Component::Normal(part) => safe_path.push(part),
            std::path::Component::CurDir => {}
            std::path::Component::RootDir | std::path::Component::Prefix(_) | std::path::Component::ParentDir => {
                bail!("unsafe archive path: {}", path.display())
            }
        }
    }
    Ok(safe_path)
}

#[cfg(unix)]
fn set_executable_permissions(path: &Path, mode: u32) -> Result<()> {
    use std::os::unix::fs::PermissionsExt;
    let executable_mode = if mode & 0o111 == 0 { 0o755 } else { mode & 0o777 };
    fs::set_permissions(path, fs::Permissions::from_mode(executable_mode))
        .with_context(|| format!("failed to set executable permissions on {}", path.display()))?;
    Ok(())
}

#[cfg(not(unix))]
fn set_executable_permissions(_path: &Path, _mode: u32) -> Result<()> {
    Ok(())
}

#[cfg(unix)]
fn zip_mode(entry: &zip::read::ZipFile<'_, File>) -> Option<u32> {
    entry.unix_mode()
}

#[cfg(not(unix))]
fn zip_mode(_entry: &zip::read::ZipFile<'_, File>) -> Option<u32> {
    None
}

#[cfg(unix)]
fn is_zip_symlink(entry: &zip::read::ZipFile<'_, File>) -> bool {
    entry.unix_mode().is_some_and(|mode| mode & 0o170000 == 0o120000)
}

#[cfg(not(unix))]
fn is_zip_symlink(_entry: &zip::read::ZipFile<'_, File>) -> bool {
    false
}

#[cfg(test)]
mod tests {
    use super::*;
    use flate2::{Compression, write::GzEncoder};
    use std::fs;
    use std::io::Write;
    use tar::{Builder, Header};
    use tempfile::tempdir;
    use zip::{ZipWriter, write::SimpleFileOptions};

    fn write_tar_gz(path: &Path, entry_name: &str, contents: &[u8]) {
        let file = File::create(path).expect("archive");
        let encoder = GzEncoder::new(file, Compression::default());
        let mut builder = Builder::new(encoder);
        let mut header = Header::new_gnu();
        header.set_path(entry_name).expect("path");
        header.set_size(contents.len() as u64);
        header.set_mode(0o755);
        header.set_cksum();
        builder.append(&header, contents).expect("entry");
        builder.into_inner().expect("tar").finish().expect("gzip");
    }

    fn write_zip(path: &Path, entry_name: &str, contents: &[u8]) {
        let file = File::create(path).expect("archive");
        let mut writer = ZipWriter::new(file);
        writer
            .start_file(entry_name, SimpleFileOptions::default().unix_permissions(0o755))
            .expect("entry");
        writer.write_all(contents).expect("contents");
        writer.finish().expect("zip");
    }

    #[test]
    fn extracts_tar_gz_binary_and_preserves_executable_permission() {
        let temp = tempdir().expect("temp");
        let archive = temp.path().join("vtcode.tar.gz");
        write_tar_gz(&archive, "vtcode-0.1.0/vtcode", b"binary");

        let extracted = extract_binary(&archive, ArchiveKind::TarGz, temp.path(), "vtcode").expect("extract");

        assert_eq!(fs::read(&extracted).expect("read"), b"binary");
        #[cfg(unix)]
        assert_eq!(fs::metadata(extracted).expect("metadata").permissions().mode() & 0o111, 0o111);
    }

    #[test]
    fn extracts_zip_binary() {
        let temp = tempdir().expect("temp");
        let archive = temp.path().join("vtcode.zip");
        write_zip(&archive, "nested/vtcode.exe", b"binary");

        let extracted = extract_binary(&archive, ArchiveKind::Zip, temp.path(), "vtcode.exe").expect("extract");

        assert_eq!(fs::read(extracted).expect("read"), b"binary");
    }

    #[test]
    fn rejects_archive_path_traversal() {
        let temp = tempdir().expect("temp");
        let archive = temp.path().join("vtcode.zip");
        write_zip(&archive, "../../vtcode", b"binary");

        let error = extract_binary(&archive, ArchiveKind::Zip, temp.path(), "vtcode").expect_err("traversal");

        assert!(error.to_string().contains("unsafe archive path"));
        assert!(!temp.path().parent().expect("parent").join("vtcode").exists());
    }

    #[test]
    fn rejects_missing_or_ambiguous_binary() {
        let temp = tempdir().expect("temp");
        let archive = temp.path().join("vtcode.tar.gz");
        write_tar_gz(&archive, "nested/not-vtcode", b"binary");

        let error = extract_binary(&archive, ArchiveKind::TarGz, temp.path(), "vtcode").expect_err("missing");

        assert!(error.to_string().contains("could not find"));
    }

    #[test]
    fn rejects_multiple_matching_binaries() {
        let temp = tempdir().expect("temp");
        let archive = temp.path().join("vtcode.zip");
        let file = File::create(&archive).expect("archive");
        let mut writer = ZipWriter::new(file);
        for path in ["first/vtcode", "second/vtcode"] {
            writer
                .start_file(path, SimpleFileOptions::default().unix_permissions(0o755))
                .expect("entry");
            writer.write_all(b"binary").expect("contents");
        }
        writer.finish().expect("zip");

        let error = extract_binary(&archive, ArchiveKind::Zip, temp.path(), "vtcode").expect_err("ambiguous");

        assert!(error.to_string().contains("multiple vtcode binaries"));
    }

    #[cfg(unix)]
    use std::os::unix::fs::PermissionsExt;
}