diff --git a/code-rs/Cargo.lock b/code-rs/Cargo.lock index 1413ef7d416..c26aa37f2c2 100644 --- a/code-rs/Cargo.lock +++ b/code-rs/Cargo.lock @@ -809,9 +809,8 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +version = "1.2.38" +source = "git+https://github.com/alexcrichton/cc-rs?rev=d740f9b1f5d65b09ccac41cac2e40caa8958e348#d740f9b1f5d65b09ccac41cac2e40caa8958e348" dependencies = [ "find-msvc-tools", "jobserver", @@ -2849,9 +2848,8 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +version = "0.1.2" +source = "git+https://github.com/alexcrichton/cc-rs?rev=d740f9b1f5d65b09ccac41cac2e40caa8958e348#d740f9b1f5d65b09ccac41cac2e40caa8958e348" [[package]] name = "fixed_decimal" diff --git a/code-rs/Cargo.toml b/code-rs/Cargo.toml index 83e8fa5171b..d2cd6d2d7b4 100644 --- a/code-rs/Cargo.toml +++ b/code-rs/Cargo.toml @@ -250,6 +250,7 @@ strip = "symbols" codegen-units = 1 [patch.crates-io] +cc = { git = "https://github.com/alexcrichton/cc-rs", rev = "d740f9b1f5d65b09ccac41cac2e40caa8958e348" } # ratatui = { path = "../../ratatui" } ratatui = { git = "https://github.com/nornagon/ratatui", branch = "nornagon-v0.29.0-patch" } diff --git a/code-rs/core/src/auth.rs b/code-rs/core/src/auth.rs index a483f7855f2..d8e18a6bb0d 100644 --- a/code-rs/core/src/auth.rs +++ b/code-rs/core/src/auth.rs @@ -310,6 +310,9 @@ fn load_auth( None => Ok(None), }; } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + return Ok(None); + } // Though if auth.json exists but is malformed, do not fall back to the // env var because the user may be expecting to use AuthMode::ChatGPT. Err(e) => { @@ -384,6 +387,9 @@ pub fn try_read_auth_json(auth_file: &Path) -> std::io::Result { } pub fn write_auth_json(auth_file: &Path, auth_dot_json: &AuthDotJson) -> std::io::Result<()> { + if let Some(parent) = auth_file.parent() { + std::fs::create_dir_all(parent)?; + } let json_data = serde_json::to_string_pretty(auth_dot_json)?; let mut options = OpenOptions::new(); options.truncate(true).write(true).create(true); @@ -519,6 +525,8 @@ mod tests { use serde::Serialize; use serde_json::json; use tempfile::tempdir; + #[cfg(unix)] + use std::os::unix::fs::PermissionsExt; const LAST_REFRESH: &str = "2025-08-06T20:41:36.232376Z"; @@ -542,6 +550,28 @@ mod tests { assert_eq!(auth_dot_json, same_auth_dot_json); } + #[test] + fn write_auth_json_creates_missing_parent_directory() { + let temp = tempdir().unwrap(); + let auth_path = temp + .path() + .join("nested") + .join("dir") + .join("auth.json"); + + let auth_dot_json = AuthDotJson { + openai_api_key: Some("sk-test".to_string()), + tokens: None, + last_refresh: None, + }; + + write_auth_json(&auth_path, &auth_dot_json).expect("write should create parent dirs"); + assert!(auth_path.exists()); + + let read_back = try_read_auth_json(&auth_path).expect("auth json should parse"); + assert_eq!(read_back.openai_api_key.as_deref(), Some("sk-test")); + } + #[test] fn login_with_api_key_overwrites_existing_auth_json() { let dir = tempdir().unwrap(); @@ -568,6 +598,50 @@ mod tests { assert!(auth.tokens.is_none(), "tokens should be cleared"); } + #[test] + fn login_with_api_key_creates_code_home_directory() { + let dir = tempdir().unwrap(); + let missing = dir.path().join("missing").join("code_home"); + + super::login_with_api_key(&missing, "sk-new") + .expect("login_with_api_key should create directory"); + + let auth = super::try_read_auth_json(&get_auth_file(&missing)).expect("auth json exists"); + assert_eq!(auth.openai_api_key.as_deref(), Some("sk-new")); + } + + #[cfg(target_os = "windows")] + #[test] + fn login_with_api_key_supports_backslash_paths() { + use std::path::PathBuf; + + let dir = tempdir().unwrap(); + let path = PathBuf::from(format!("{}\\nested\\code_home", dir.path().display())); + + super::login_with_api_key(&path, "sk-windows") + .expect("login_with_api_key should succeed on Windows-style path"); + + let auth_path = get_auth_file(&path); + assert!(auth_path.exists(), "auth.json should be created for Windows path"); + } + + #[cfg(target_os = "macos")] + #[test] + fn login_with_api_key_handles_paths_with_spaces() { + let dir = tempdir().unwrap(); + let path = dir + .path() + .join("Library") + .join("Application Support") + .join("Code Home"); + + super::login_with_api_key(&path, "sk-macos") + .expect("login_with_api_key should succeed for paths with spaces"); + + let auth = super::try_read_auth_json(&get_auth_file(&path)).expect("auth json exists"); + assert_eq!(auth.openai_api_key.as_deref(), Some("sk-macos")); + } + #[tokio::test] async fn pro_account_with_no_api_key_uses_chatgpt_auth() { let code_home = tempdir().unwrap(); @@ -718,6 +792,76 @@ mod tests { assert!(auth.get_token_data().await.is_err()); } + #[test] + fn load_auth_returns_none_when_auth_json_missing() { + let dir = tempdir().unwrap(); + + let result = super::load_auth(dir.path(), false, AuthMode::ChatGPT, "code_cli_rs") + .expect("missing auth.json should not error"); + + assert!(result.is_none(), "missing auth.json should return None"); + } + + #[cfg(unix)] + #[test] + fn write_auth_json_propagates_permission_denied_unix() { + let dir = tempdir().unwrap(); + let read_only_dir = dir.path().join("readonly"); + std::fs::create_dir(&read_only_dir).unwrap(); + let mut perms = std::fs::metadata(&read_only_dir).unwrap().permissions(); + perms.set_mode(0o500); + std::fs::set_permissions(&read_only_dir, perms).unwrap(); + + let auth_path = read_only_dir.join("nested").join("auth.json"); + let result = write_auth_json( + &auth_path, + &AuthDotJson { + openai_api_key: Some("sk-unix".to_string()), + tokens: None, + last_refresh: None, + }, + ); + + assert_eq!( + result.unwrap_err().kind(), + std::io::ErrorKind::PermissionDenied + ); + } + + #[test] + fn write_auth_json_fails_when_file_is_readonly() { + let dir = tempdir().unwrap(); + let auth_path = dir.path().join("auth.json"); + + write_auth_json( + &auth_path, + &AuthDotJson { + openai_api_key: Some("sk-initial".to_string()), + tokens: None, + last_refresh: None, + }, + ) + .expect("initial write succeeds"); + + let mut perms = std::fs::metadata(&auth_path).unwrap().permissions(); + perms.set_readonly(true); + std::fs::set_permissions(&auth_path, perms.clone()).unwrap(); + + let result = write_auth_json( + &auth_path, + &AuthDotJson { + openai_api_key: Some("sk-readonly".to_string()), + tokens: None, + last_refresh: None, + }, + ); + + assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::PermissionDenied); + + perms.set_readonly(false); + std::fs::set_permissions(&auth_path, perms).unwrap(); + } + #[test] fn logout_removes_auth_file() -> Result<(), std::io::Error> { let dir = tempdir()?;