girt 0.11.3

An incremental, idiomatic Rust implementation of Git
Documentation
use rstest::rstest;

use super::*;
use crate::remote::{ProtocolEnvironment, Remote};

fn configured(config: &[u8], environment: SshEnvironment) -> Result<SshRemote, SshError> {
    let config = Config::parse(config).unwrap();
    let destination = Remote::find(&config, b"r")
        .unwrap()
        .unwrap()
        .fetch_destination(&config, &ProtocolEnvironment::default())
        .unwrap();
    SshRemote::configured(&config, &destination, environment)
}

fn default_environment() -> SshEnvironment {
    SshEnvironment {
        default_executable: "/usr/bin/ssh".into(),
        config_file: "/dev/null".into(),
        default_user: "default".into(),
        ..SshEnvironment::default()
    }
}

#[test]
fn configured_endpoint_and_command_precedence() {
    let mut environment = default_environment();
    environment.git_ssh = Some(b"/agent/ssh".to_vec());
    environment.git_ssh_command = Some(b"/agent/ssh -i /agent/key".to_vec());
    environment.approved_command = Some(ApprovedSshCommand {
        configured: b"/agent/ssh -i /agent/key".to_vec(),
        executable: "/agent/ssh".into(),
        arguments: vec!["-i".into(), "/agent/key".into()],
    });
    let remote = configured(
        b"[remote \"r\"]\nurl = ssh://user@host.test:2222/repo\n[core]\nsshCommand = /other/ssh\n",
        environment,
    )
    .unwrap();
    let command = remote.command("git-upload-pack");
    assert_eq!(command.get_program(), "/agent/ssh");
    let args: Vec<_> = command.get_args().collect();
    assert!(args.windows(2).any(|pair| pair == ["-i", "/agent/key"]));
    assert!(args.windows(2).any(|pair| pair == ["-p", "2222"]));
    assert!(args.windows(2).any(|pair| pair == ["-l", "user"]));
    assert_eq!(*args.last().unwrap(), "git-upload-pack '/repo'");
    assert!(!format!("{remote:?}").contains("/agent/key"));
}

#[test]
fn unapproved_command_and_variant_are_refused_without_leaking_values() {
    let mut environment = default_environment();
    environment.git_ssh_command = Some(b"secret -oUnsafe=yes".to_vec());
    let error = configured(b"[remote \"r\"]\nurl = host:repo\n", environment).unwrap_err();
    assert!(matches!(error, SshError::Configuration(_)));
    assert!(!format!("{error:?} {error}").contains("secret"));

    let mut environment = default_environment();
    environment.git_ssh_variant = Some(b"plink".to_vec());
    let error = configured(b"[remote \"r\"]\nurl = host:repo\n", environment).unwrap_err();
    assert!(matches!(error, SshError::Configuration("SSH variant")));
}

#[test]
fn config_command_requires_exact_approval() {
    let source =
        b"[remote \"r\"]\nurl = host:repo\n[core]\nsshCommand = /approved/ssh -i /approved/key\n";
    let error = configured(source, default_environment()).unwrap_err();
    assert!(matches!(error, SshError::Configuration(_)));
    let mut environment = default_environment();
    environment.approved_command = Some(ApprovedSshCommand {
        configured: b"/approved/ssh -i /other/key".to_vec(),
        executable: "/approved/ssh".into(),
        arguments: Vec::new(),
    });
    let error = configured(source, environment).unwrap_err();
    assert!(matches!(
        error,
        SshError::Configuration("SSH command approval mismatch")
    ));

    let mut environment = default_environment();
    environment.approved_command = Some(ApprovedSshCommand {
        configured: b"/approved/ssh -i /approved/key".to_vec(),
        executable: "/approved/ssh".into(),
        arguments: vec!["-i".into(), "/approved/key".into()],
    });
    let remote = configured(source, environment).unwrap();
    assert_eq!(
        remote.command("git-upload-pack").get_program(),
        "/approved/ssh"
    );
}

#[rstest]
#[case::percent(b"[remote \"r\"]\nurl = ssh://host/repo%20secret\n")]
#[case::userinfo(b"[remote \"r\"]\nurl = ssh://u:p@host/repo\n")]
#[case::port(b"[remote \"r\"]\nurl = ssh://host:0/repo\n")]
#[case::query(b"[remote \"r\"]\nurl = ssh://host/repo?secret\n")]
fn configured_refuses_unsupported_endpoint_shapes(#[case] source: &[u8]) {
    let error = configured(source, default_environment()).unwrap_err();
    assert!(matches!(error, SshError::Configuration(_)));
    assert!(!format!("{error:?} {error}").contains("secret"));
}

#[test]
fn agent_and_askpass_are_explicit_and_redacted() {
    let mut environment = default_environment();
    environment.agent_socket = Some("/secret/socket".into());
    environment.askpass = Some("/secret/askpass".into());
    let remote = configured(b"[remote \"r\"]\nurl = host:repo\n", environment).unwrap();
    let command = remote.command("git-upload-pack");
    let env: Vec<_> = command.get_envs().collect();
    assert!(env.iter().any(|(name, value)| {
        *name == "SSH_AUTH_SOCK" && *value == Some(std::ffi::OsStr::new("/secret/socket"))
    }));
    assert!(env.iter().any(|(name, value)| {
        *name == "SSH_ASKPASS" && *value == Some(std::ffi::OsStr::new("/secret/askpass"))
    }));
    let args: Vec<_> = command.get_args().collect();
    assert!(args.windows(2).any(|pair| pair == ["-o", "BatchMode=no"]));
    assert!(
        args.windows(2)
            .any(|pair| pair == ["-o", "IdentityAgent=SSH_AUTH_SOCK"])
    );
    assert!(!format!("{remote:?}").contains("secret"));
}

#[test]
fn configured_input_debug_hides_secrets() {
    let mut environment = default_environment();
    environment.git_ssh_command = Some(b"secret-command".to_vec());
    environment.agent_socket = Some("/secret/socket".into());
    environment.approved_command = Some(ApprovedSshCommand {
        configured: b"secret-command".to_vec(),
        executable: "/secret/ssh".into(),
        arguments: vec!["secret-argument".into()],
    });
    assert!(!format!("{environment:?}").contains("secret"));
    assert!(!format!("{:?}", environment.approved_command.as_ref().unwrap()).contains("secret"));
}

#[rstest]
#[case::option("-oProxyCommand=secret", "user", "repo")]
#[case::url("ssh://host", "user", "repo")]
#[case::scp("user@host:repo", "user", "repo")]
#[case::user("host", "-secret", "repo")]
#[case::path_option("host", "user", "--secret")]
#[case::tilde("host", "user", "~/secret")]
#[case::newline("host", "user", "repo\nsecret")]
#[case::empty("", "user", "repo")]
fn rejects_unsafe_components(#[case] host: &str, #[case] user: &str, #[case] path: &str) {
    let error = SshRemote::new(
        host,
        user,
        22,
        path,
        Path::new("/usr/bin/ssh"),
        Path::new("/dev/null"),
    )
    .unwrap_err();
    assert!(matches!(error, SshError::Configuration(_)));
    assert!(!format!("{error:?} {error}").contains("secret"));
}
#[test]
fn quotes_path_and_separates_options() {
    let remote = SshRemote::new(
        "::1",
        "user",
        22,
        "repo ' ; $(secret)",
        Path::new("/usr/bin/ssh"),
        Path::new("/dev/null"),
    )
    .unwrap();
    let command = remote.command("git-upload-pack");
    let args: Vec<_> = command.get_args().collect();
    assert_eq!(
        *args.last().unwrap(),
        "git-upload-pack 'repo '\\'' ; $(secret)'"
    );
    assert_eq!(args[args.len() - 3], "--");
    assert!(!format!("{remote:?}").contains("secret"));
}
#[rstest]
#[case::zero_port(0, "/usr/bin/ssh", "/dev/null")]
#[case::relative_executable(22, "ssh", "/dev/null")]
#[case::relative_config(22, "/usr/bin/ssh", "config")]
fn rejects_unsupported_configuration(#[case] port: u16, #[case] exe: &str, #[case] config: &str) {
    assert!(
        SshRemote::new(
            "host",
            "user",
            port,
            "repo",
            Path::new(exe),
            Path::new(config)
        )
        .is_err()
    );
}

#[rstest]
#[case::config_over_legacy(None, Some(b"legacy".as_slice()), "configured")]
#[case::command_over_config(Some(b"command".as_slice()), Some(b"legacy".as_slice()), "command")]
#[case::config_without_legacy(None, None, "configured")]
fn selects_highest_priority_command(
    #[case] command: Option<&[u8]>,
    #[case] legacy: Option<&[u8]>,
    #[case] selected: &str,
) {
    let environment = SshEnvironment {
        git_ssh_command: command.map(Vec::from),
        git_ssh: legacy.map(Vec::from),
        approved_command: Some(ApprovedSshCommand {
            configured: selected.as_bytes().to_vec(),
            executable: PathBuf::from(format!("/approved/{selected}")),
            arguments: Vec::new(),
        }),
        ..default_environment()
    };
    let remote = configured(
        b"[remote \"r\"]\nurl = host:repo\n[core]\nsshCommand = old\nsshCommand = configured\n",
        environment,
    )
    .unwrap();
    assert_eq!(
        remote.command("git-upload-pack").get_program(),
        std::ffi::OsStr::new(&format!("/approved/{selected}"))
    );
}

#[rstest]
#[case::command(b"[core]\nsshCommand\n")]
#[case::variant(b"[ssh]\nvariant\n")]
fn rejects_selected_valueless_configuration(#[case] setting: &[u8]) {
    let source = [b"[remote \"r\"]\nurl = host:repo\n".as_slice(), setting].concat();
    let error = configured(&source, default_environment()).unwrap_err();
    assert!(matches!(
        error,
        SshError::Configuration("SSH configuration requires a value")
    ));
}

#[test]
fn environment_overrides_valueless_configuration() {
    let environment = SshEnvironment {
        git_ssh_command: Some(b"approved".to_vec()),
        git_ssh_variant: Some(b"SSH".to_vec()),
        approved_command: Some(ApprovedSshCommand {
            configured: b"approved".to_vec(),
            executable: "/approved/ssh".into(),
            arguments: Vec::new(),
        }),
        ..default_environment()
    };
    let remote = configured(
        b"[remote \"r\"]\nurl = host:repo\n[core]\nsshCommand\n[ssh]\nvariant\n",
        environment,
    )
    .unwrap();
    assert_eq!(
        remote.command("git-upload-pack").get_program(),
        "/approved/ssh"
    );
}

#[rstest]
#[case::environment_command(Some(Vec::new()), Some(b"legacy".to_vec()), "")]
#[case::config_command(None, Some(b"legacy".to_vec()), "[core]\nsshCommand =\n")]
#[case::legacy_command(None, Some(Vec::new()), "")]
fn empty_command_does_not_fall_through(
    #[case] command: Option<Vec<u8>>,
    #[case] legacy: Option<Vec<u8>>,
    #[case] setting: &str,
) {
    let environment = SshEnvironment {
        git_ssh_command: command,
        git_ssh: legacy,
        ..default_environment()
    };
    let source = format!("[remote \"r\"]\nurl = host:repo\n{setting}");
    let error = configured(source.as_bytes(), environment).unwrap_err();
    assert!(matches!(
        error,
        SshError::Configuration("empty SSH command")
    ));
}

#[rstest]
#[case::environment_wins(Some(b"ssh".as_slice()), "plink", true)]
#[case::environment_refused(Some(b"plink".as_slice()), "ssh", false)]
#[case::case_insensitive(None, "SSH", true)]
#[case::auto_is_unsupported(None, "auto", false)]
#[case::simple_is_unsupported(None, "simple", false)]
#[case::unknown_is_unsupported(None, "unknown", false)]
#[case::empty_is_unsupported(Some(b"".as_slice()), "ssh", false)]
fn variant_selection_is_explicit(
    #[case] variant: Option<&[u8]>,
    #[case] setting: &str,
    #[case] supported: bool,
) {
    let environment = SshEnvironment {
        git_ssh_variant: variant.map(Vec::from),
        ..default_environment()
    };
    let source = format!("[remote \"r\"]\nurl = host:repo\n[ssh]\nvariant = {setting}\n");
    let result = configured(source.as_bytes(), environment);
    assert_eq!(result.is_ok(), supported);
}

#[test]
fn lower_priority_approval_cannot_authorize_selected_command() {
    let environment = SshEnvironment {
        git_ssh: Some(b"legacy".to_vec()),
        approved_command: Some(ApprovedSshCommand {
            configured: b"legacy".to_vec(),
            executable: "/approved/ssh".into(),
            arguments: Vec::new(),
        }),
        ..default_environment()
    };
    let error = configured(
        b"[remote \"r\"]\nurl = host:repo\n[core]\nsshCommand = selected\n",
        environment,
    )
    .unwrap_err();
    assert!(matches!(
        error,
        SshError::Configuration("SSH command approval mismatch")
    ));
}

#[test]
fn rejects_trailing_bytes_after_bracketed_host() {
    let result = parse_endpoint(b"ssh://[::1]unexpected/repo");
    assert!(matches!(result, Err(SshError::Configuration("SSH host"))));
}

/// Approved arguments keep the existing size and control-character bounds.
#[rstest]
#[case::plain("-i", true)]
#[case::empty("", false)]
#[case::newline("a\nb", false)]
#[case::c1_control("a\u{85}b", false)]
#[case::at_limit(&"x".repeat(8192), true)]
#[case::too_long(&"x".repeat(8193), false)]
fn approved_argument_bounds(#[case] argument: &str, #[case] valid: bool) {
    assert_eq!(valid_argument(std::ffi::OsStr::new(argument)), valid);
}

/// Arguments that are not Unicode keep the size bound and reject ASCII control bytes.
#[cfg(unix)]
#[rstest]
#[case::plain(b"/keys/\xff", true)]
#[case::escape(b"\xff\x1b", false)]
#[case::nul(b"\xff\0", false)]
#[case::delete(b"\xff\x7f", false)]
fn approved_non_unicode_argument_bounds(#[case] argument: &[u8], #[case] valid: bool) {
    use std::os::unix::ffi::OsStrExt;

    assert_eq!(valid_argument(std::ffi::OsStr::from_bytes(argument)), valid);
}

/// An approved key path that is not UTF-8 reaches the built command's arguments unchanged.
#[cfg(unix)]
#[test]
fn approved_argument_keeps_non_utf8_bytes() {
    use std::os::unix::ffi::OsStrExt;

    let key = std::ffi::OsStr::from_bytes(b"/keys/\xff-deploy");
    assert!(valid_argument(key));

    let mut environment = default_environment();
    environment.git_ssh_command = Some(b"ssh -i key".to_vec());
    environment.approved_command = Some(ApprovedSshCommand::new(
        b"ssh -i key".to_vec(),
        "/usr/bin/ssh".into(),
        vec!["-i".into(), key.to_owned()],
    ));
    let remote = configured(
        b"[remote \"r\"]\nurl = ssh://example.com/repo.git\n",
        environment,
    )
    .unwrap();
    let command = remote.command("git-upload-pack");
    let args: Vec<_> = command.get_args().collect();
    assert!(
        args.windows(2)
            .any(|pair| pair == [std::ffi::OsStr::new("-i"), key])
    );
}