mirror of
https://github.com/openai/codex.git
synced 2026-09-14 11:57:03 +00:00
## Why External-agent migration treated symlinked empty text targets as overwritable files. Writing migrated configuration through such a target could modify a file outside the repository. ## What changed - Use symlink metadata when checking whether a migration target is missing or empty, so only regular files are considered overwritable. - Preserve symlinked `AGENTS.md` and `.codex/hooks.json` targets during both detection and import. ## Testing Added Unix regression coverage for existing and dangling symlink targets for both guidance and hooks migration. See https://github.com/openai/codex/pull/26021. GitOrigin-RevId: 13c78e7458d1c02abdb22b38ba88ed66bb5a154b
96 lines
2.5 KiB
Rust
96 lines
2.5 KiB
Rust
use crate::RewriteProfile;
|
|
use serde_json::Value as JsonValue;
|
|
use std::fs;
|
|
use std::io;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
pub(super) fn display_source_paths(paths: &[PathBuf]) -> String {
|
|
paths
|
|
.iter()
|
|
.map(|path| path.display().to_string())
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
}
|
|
|
|
pub(crate) fn read_json_file(path: &Path) -> io::Result<Option<JsonValue>> {
|
|
if !path.is_file() {
|
|
return Ok(None);
|
|
}
|
|
|
|
let raw = fs::read_to_string(path)?;
|
|
serde_json::from_str(&raw)
|
|
.map(Some)
|
|
.map_err(|err| invalid_data_error(err.to_string()))
|
|
}
|
|
|
|
pub(super) fn is_missing_or_empty_text_file(path: &Path) -> io::Result<bool> {
|
|
let metadata = match fs::symlink_metadata(path) {
|
|
Ok(metadata) => metadata,
|
|
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(true),
|
|
Err(err) => return Err(err),
|
|
};
|
|
if !metadata.is_file() {
|
|
return Ok(false);
|
|
}
|
|
|
|
Ok(fs::read_to_string(path)?.trim().is_empty())
|
|
}
|
|
|
|
pub(super) fn invalid_data_error(message: impl Into<String>) -> io::Error {
|
|
io::Error::new(io::ErrorKind::InvalidData, message.into())
|
|
}
|
|
|
|
pub(super) fn copy_dir_recursive(
|
|
source: &Path,
|
|
target: &Path,
|
|
rewrite_profile: RewriteProfile,
|
|
) -> io::Result<()> {
|
|
fs::create_dir_all(target)?;
|
|
|
|
for entry in fs::read_dir(source)? {
|
|
let entry = entry?;
|
|
let source_path = entry.path();
|
|
let target_path = target.join(entry.file_name());
|
|
let file_type = entry.file_type()?;
|
|
|
|
if file_type.is_dir() {
|
|
copy_dir_recursive(&source_path, &target_path, rewrite_profile)?;
|
|
continue;
|
|
}
|
|
|
|
if file_type.is_file() {
|
|
if is_skill_md(&source_path) {
|
|
rewrite_and_copy_text_file(&source_path, &target_path, rewrite_profile)?;
|
|
} else {
|
|
fs::copy(source_path, target_path)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn rewrite_external_agent_terms(
|
|
content: &str,
|
|
rewrite_profile: RewriteProfile,
|
|
) -> String {
|
|
rewrite_profile.rewrite(content)
|
|
}
|
|
|
|
fn is_skill_md(path: &Path) -> bool {
|
|
path.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.is_some_and(|name| name.eq_ignore_ascii_case("SKILL.md"))
|
|
}
|
|
|
|
fn rewrite_and_copy_text_file(
|
|
source: &Path,
|
|
target: &Path,
|
|
rewrite_profile: RewriteProfile,
|
|
) -> io::Result<()> {
|
|
let source_contents = fs::read_to_string(source)?;
|
|
let rewritten = rewrite_external_agent_terms(&source_contents, rewrite_profile);
|
|
fs::write(target, rewritten)
|
|
}
|