fix: enforce complete external edit result contract
This commit is contained in:
@@ -337,6 +337,11 @@ pub trait FsAccessPolicy: Send + Sync {
|
||||
None
|
||||
}
|
||||
|
||||
/// Bound provider-owned Edit result cardinality before mutation.
|
||||
fn max_edit_replacements(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Atomically replace or create an already-authorized writable file.
|
||||
/// Capability providers override this to keep parent traversal descriptor-
|
||||
/// confined through the final rename.
|
||||
@@ -521,6 +526,33 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateRaceAccess {
|
||||
root: PathBuf,
|
||||
destination: PathBuf,
|
||||
}
|
||||
|
||||
impl FsAccessPolicy for CreateRaceAccess {
|
||||
fn is_readable(&self, path: &Path) -> bool {
|
||||
path.starts_with(&self.root)
|
||||
}
|
||||
|
||||
fn is_writable(&self, path: &Path) -> bool {
|
||||
path.starts_with(&self.root)
|
||||
}
|
||||
|
||||
fn read_metadata(
|
||||
&self,
|
||||
_logical: &Path,
|
||||
_resolved: &Path,
|
||||
) -> std::io::Result<std::fs::Metadata> {
|
||||
std::fs::write(&self.destination, b"created concurrently")?;
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"simulated absence before create race",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn grep_request(path: &str, pattern: &str) -> GrepRequest {
|
||||
GrepRequest {
|
||||
pattern: pattern.to_string(),
|
||||
@@ -1175,6 +1207,34 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_clobber_create_race_is_reported_as_conflict() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let destination = root.path().join("race.txt");
|
||||
let root_path = root.path().canonicalize().unwrap();
|
||||
let access = CreateRaceAccess {
|
||||
root: root_path.clone(),
|
||||
destination: destination.clone(),
|
||||
};
|
||||
|
||||
let error = run_write(
|
||||
&root_path,
|
||||
WriteRequest {
|
||||
path: FsPath::new("race.txt").unwrap(),
|
||||
content: b"worker content".to_vec(),
|
||||
expected_hash: None,
|
||||
},
|
||||
&access,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(error, FsError::Conflict(path) if path == "race.txt"));
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(destination).unwrap(),
|
||||
"created concurrently"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn descriptor_confined_atomic_write_rejects_symlinks_and_survives_root_rename() {
|
||||
|
||||
@@ -231,7 +231,13 @@ pub fn run_write(
|
||||
}
|
||||
access
|
||||
.atomic_write_file(&path, &target, &request.content, AtomicWriteMode::CreateNew)
|
||||
.map_err(|error| map_io(&logical, error))?;
|
||||
.map_err(|error| {
|
||||
if error.kind() == std::io::ErrorKind::AlreadyExists {
|
||||
FsError::Conflict(logical.as_str().to_string())
|
||||
} else {
|
||||
map_io(&logical, error)
|
||||
}
|
||||
})?;
|
||||
}
|
||||
Ok(WriteResult {
|
||||
bytes_written: request.content.len(),
|
||||
@@ -276,6 +282,14 @@ pub fn run_edit(
|
||||
)));
|
||||
}
|
||||
let replacement_count = if request.replace_all { occurrences } else { 1 };
|
||||
if let Some(limit) = access.max_edit_replacements()
|
||||
&& replacement_count > limit
|
||||
{
|
||||
return Err(FsError::InvalidArgument(format!(
|
||||
"{} exceeds provider Edit replacement limit {limit}",
|
||||
logical.as_str()
|
||||
)));
|
||||
}
|
||||
let removed_bytes = request
|
||||
.old_string
|
||||
.len()
|
||||
|
||||
Reference in New Issue
Block a user