|
2 | 2 |
|
3 | 3 | use anyhow::{Context, Result}; |
4 | 4 | use std::collections::{HashMap, HashSet}; |
5 | | -use std::path::Path; |
| 5 | +use std::path::{Path, PathBuf}; |
6 | 6 |
|
7 | 7 | use super::extensions::{ |
8 | 8 | CompilerExtension, Declarations, McpgConfig, McpgGatewayConfig, McpgServerConfig, |
@@ -45,21 +45,26 @@ pub async fn atomic_write(path: &Path, contents: &str) -> Result<()> { |
45 | 45 | .context("atomic_write task panicked")? |
46 | 46 | } |
47 | 47 |
|
| 48 | +/// Returns the directory in which the atomic tempfile should be created for a |
| 49 | +/// write to `path`. The tempfile must live on the same filesystem as `path` |
| 50 | +/// so that the final `persist()` rename is atomic (EXDEV guard). |
| 51 | +/// |
| 52 | +/// - `path.parent() == Some(non-empty)` → use that parent directory. |
| 53 | +/// - `path.parent() == Some("")` (bare filename like `"agent.md"`) or `None` |
| 54 | +/// → use `"."` (current directory), which is on the same filesystem as a |
| 55 | +/// bare-filename destination. |
| 56 | +pub(crate) fn atomic_write_parent_dir(path: &Path) -> PathBuf { |
| 57 | + let parent = path.parent().filter(|p| !p.as_os_str().is_empty()); |
| 58 | + parent |
| 59 | + .map(|p| p.to_owned()) |
| 60 | + .unwrap_or_else(|| PathBuf::from(".")) |
| 61 | +} |
| 62 | + |
48 | 63 | fn atomic_write_blocking(path: &Path, contents: &str) -> Result<()> { |
49 | 64 | use std::io::Write; |
50 | 65 |
|
51 | | - // Determine the directory to create the tempfile in. We MUST use |
52 | | - // a path on the same filesystem as the destination so that the |
53 | | - // final `persist` rename is atomic (otherwise it fails with |
54 | | - // EXDEV on Linux when /tmp is a separate tmpfs mount). |
55 | | - // |
56 | | - // - `path.parent() == Some(non-empty)` -> use that parent. |
57 | | - // - `path.parent() == Some("")` (bare filename like "agent.md") |
58 | | - // or `None` -> use the current directory ("."), which is the |
59 | | - // same filesystem as where the file will land. |
60 | | - let parent = path.parent().filter(|p| !p.as_os_str().is_empty()); |
61 | | - let parent_dir: &Path = parent.unwrap_or_else(|| Path::new(".")); |
62 | | - let mut tmp = tempfile::NamedTempFile::new_in(parent_dir).with_context(|| { |
| 66 | + let parent_dir = atomic_write_parent_dir(path); |
| 67 | + let mut tmp = tempfile::NamedTempFile::new_in(&parent_dir).with_context(|| { |
63 | 68 | format!( |
64 | 69 | "failed to create temporary file in {}", |
65 | 70 | parent_dir.display() |
@@ -3284,28 +3289,17 @@ mod tests { |
3284 | 3289 | } |
3285 | 3290 |
|
3286 | 3291 | #[test] |
3287 | | - fn atomic_write_parent_derivation_handles_bare_filename() { |
| 3292 | + fn atomic_write_parent_dir_handles_bare_filename() { |
3288 | 3293 | // Regression test for the EXDEV bug: a bare filename (no |
3289 | 3294 | // directory component) used to fall through to |
3290 | 3295 | // `NamedTempFile::new()` which creates the tempfile in the |
3291 | 3296 | // system temp dir, breaking persist() across filesystem |
3292 | | - // boundaries (e.g. tmpfs `/tmp` on Linux). Verify the |
3293 | | - // derivation logic now picks ".". Pure-logic test — no |
3294 | | - // filesystem I/O so it's parallel-safe. |
3295 | | - let bare = Path::new("agent.md"); |
3296 | | - let parent = bare.parent().filter(|p| !p.as_os_str().is_empty()); |
3297 | | - let parent_dir: &Path = parent.unwrap_or_else(|| Path::new(".")); |
3298 | | - assert_eq!(parent_dir, Path::new(".")); |
3299 | | - |
3300 | | - let with_dir = Path::new("subdir/agent.md"); |
3301 | | - let parent = with_dir.parent().filter(|p| !p.as_os_str().is_empty()); |
3302 | | - let parent_dir: &Path = parent.unwrap_or_else(|| Path::new(".")); |
3303 | | - assert_eq!(parent_dir, Path::new("subdir")); |
3304 | | - |
3305 | | - let absolute = Path::new("/tmp/agent.md"); |
3306 | | - let parent = absolute.parent().filter(|p| !p.as_os_str().is_empty()); |
3307 | | - let parent_dir: &Path = parent.unwrap_or_else(|| Path::new(".")); |
3308 | | - assert_eq!(parent_dir, Path::new("/tmp")); |
| 3297 | + // boundaries (e.g. tmpfs `/tmp` on Linux). Verify that the |
| 3298 | + // extracted helper returns "." for a bare filename so the |
| 3299 | + // tempfile lands on the same filesystem as the destination. |
| 3300 | + assert_eq!(atomic_write_parent_dir(Path::new("agent.md")), PathBuf::from(".")); |
| 3301 | + assert_eq!(atomic_write_parent_dir(Path::new("subdir/agent.md")), PathBuf::from("subdir")); |
| 3302 | + assert_eq!(atomic_write_parent_dir(Path::new("/tmp/agent.md")), PathBuf::from("/tmp")); |
3309 | 3303 | } |
3310 | 3304 |
|
3311 | 3305 | // ─── parse_markdown_detailed ────────────────────────────────────────────── |
|
0 commit comments