use std::path::Path;
#[cfg(not(any(feature = "cli", feature = "files")))]
use anyhow::{Context, bail};
use crate::containment::PathGuard;
use crate::plan::Operation;
use super::{ApplyMode, EditResult};
#[cfg(any(feature = "cli", feature = "files"))]
fn cwd_from_path(path: &Path) -> &Path {
path.parent().unwrap_or_else(|| Path::new("."))
}
#[cfg(any(feature = "cli", feature = "files"))]
fn abs_path(path: &Path) -> anyhow::Result<std::path::PathBuf> {
super::absolute_for_engine(path).map_err(|e| {
crate::fallback::EditError::new(
crate::fallback::EditErrorKind::OperationFailed,
format!("failed to resolve path {}: {e}", path.display()),
)
.into()
})
}
#[cfg(any(feature = "cli", feature = "files"))]
fn file_write(
op: Operation,
path: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
action: &'static str,
) -> anyhow::Result<EditResult> {
let display = path.to_string_lossy();
super::execute_as_edit_result_with_path(
op,
mode,
cwd_from_path(path),
guard,
action,
None,
Some(display.as_ref()),
)
}
#[cfg(not(any(feature = "cli", feature = "files")))]
fn file_write(
op: Operation,
path: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
action: &'static str,
) -> anyhow::Result<EditResult> {
match op {
Operation::FileCreate { content, force, .. } => {
let path_str = path.to_string_lossy();
use crate::ops::file::{PathEntryKind, classify_path_entry, path_entry_exists};
match classify_path_entry(path) {
PathEntryKind::RealDirectory => {
return Err(anyhow::Error::new(crate::exit::InvalidInputError {
msg: format!("target is not a file: {}", path.display()),
}));
}
PathEntryKind::Missing | PathEntryKind::RegularFile | PathEntryKind::Special => {}
}
crate::ops::file::ensure_parent_components_are_directories(path)?;
let force = force.unwrap_or(false);
if !force && path_entry_exists(path) {
return Err(anyhow::Error::new(crate::exit::AlreadyExistsError {
msg: format!(
"file already exists: {} (use force to overwrite)",
path.display()
),
}));
}
let original = match classify_path_entry(path) {
PathEntryKind::RegularFile => {
match crate::files::load_text_strict(path, &path_str) {
Ok(s) => s,
Err(e) if force && crate::exit::is_load_text_strict_fail(&e) => {
String::new()
}
Err(e) => return Err(e),
}
}
PathEntryKind::Missing | PathEntryKind::Special | PathEntryKind::RealDirectory => {
String::new()
}
};
let policy = crate::write::WritePolicy::default();
let (applied, backup_session) =
super::write_if_apply(path, &content, mode, &policy, guard)?;
{
let mut __e =
super::build_edit_result(&path_str, original, content, applied, action, None);
__e.backup_session = backup_session;
Ok(__e)
}
}
Operation::FileDelete { .. } => {
let path_str = path.to_string_lossy();
if !crate::ops::file::path_entry_exists(path) {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("file not found: {}", path.display()),
)
.into());
}
crate::ops::file::ensure_unlinkable_not_directory(path, path_str.as_ref())?;
let original = if crate::ops::file::is_regular_file_for_backup(path) {
crate::files::load_text_strict(path, &path_str).unwrap_or_default()
} else {
String::new()
};
let (applied, backup_session) = if mode == ApplyMode::Apply {
super::apply_mutation(
path,
mode,
guard,
|backup| backup.save_before_delete(path),
|| {
std::fs::remove_file(path)
.with_context(|| format!("failed to delete {}", path.display()))
},
)?
} else {
super::ensure_contained(guard, path)?;
(false, None)
};
{
let mut __e = super::build_edit_result(
&path_str,
original,
String::new(),
applied,
action,
None,
);
__e.backup_session = backup_session;
Ok(__e)
}
}
Operation::FileAppend { ref content, .. } | Operation::FilePrepend { ref content, .. } => {
let is_append = matches!(op, Operation::FileAppend { .. });
let content = content.clone();
let path_str = path.to_string_lossy();
use crate::ops::file::{PathEntryKind, classify_path_entry, path_entry_exists};
if !path_entry_exists(path) {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("file does not exist: {}", path.display()),
)
.into());
}
if classify_path_entry(path) != PathEntryKind::RegularFile {
return Err(anyhow::Error::new(crate::exit::InvalidInputError {
msg: format!("target is not a file: {}", path.display()),
}));
}
let original = crate::files::load_text_strict(path, &path_str)?;
let combined = if is_append {
crate::ops::file::append_content(&original, &content)
} else {
crate::ops::file::prepend_content(&original, &content)
};
let policy = crate::write::WritePolicy::default();
let (applied, backup_session) =
super::write_if_apply(path, &combined, mode, &policy, guard)?;
{
let mut __e =
super::build_edit_result(&path_str, original, combined, applied, action, None);
__e.backup_session = backup_session;
Ok(__e)
}
}
_ => bail!("unsupported file operation"),
}
}
#[cfg(any(feature = "cli", feature = "files"))]
fn file_write_cross(
op: Operation,
src: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
action: &'static str,
dest_path: Option<String>,
) -> anyhow::Result<EditResult> {
let display = src.to_string_lossy();
super::execute_as_edit_result_with_path(
op,
mode,
cwd_from_path(src),
guard,
action,
dest_path,
Some(display.as_ref()),
)
}
#[cfg(not(any(feature = "cli", feature = "files")))]
fn file_write_cross(
_op: Operation,
src: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
action: &'static str,
dest_path: Option<String>,
) -> anyhow::Result<EditResult> {
if let Operation::FileRename { to, force, .. } = _op {
let dst = Path::new(&to);
if !force && dst.exists() {
return Err(anyhow::Error::new(crate::exit::AlreadyExistsError {
msg: format!(
"destination already exists: {} (use force to overwrite)",
dst.display()
),
}));
}
let original = crate::files::try_read_text_file(src).unwrap_or_default();
let (applied, backup_session) = super::apply_cross_file_mutation(
src,
Some(dst),
mode,
guard,
|backup| {
backup.save_before_write(src)?;
if dst.exists() && force {
backup.save_before_write(dst)?;
}
Ok(())
},
|| {
std::fs::rename(src, dst).with_context(|| {
format!("failed to rename {} -> {}", src.display(), dst.display())
})
},
)?;
{
let mut __e = super::build_edit_result(
&src.to_string_lossy(),
original.clone(),
original,
applied,
action,
dest_path,
);
__e.backup_session = backup_session;
Ok(__e)
}
} else {
bail!("unsupported cross-file operation")
}
}
pub fn file_create(
path: &Path,
content: &str,
force: bool,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
#[cfg(any(feature = "cli", feature = "files"))]
let path_owned = abs_path(path)?;
#[cfg(any(feature = "cli", feature = "files"))]
let path = path_owned.as_path();
let op = Operation::FileCreate {
path: path.to_string_lossy().into(),
content: content.into(),
force: Some(force),
};
file_write(op, path, mode, guard, "create")
}
pub fn file_delete(
path: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
#[cfg(any(feature = "cli", feature = "files"))]
let path_owned = abs_path(path)?;
#[cfg(any(feature = "cli", feature = "files"))]
let path = path_owned.as_path();
let op = Operation::FileDelete {
path: path.to_string_lossy().into(),
};
file_write(op, path, mode, guard, "delete")
}
pub fn file_rename(
src: &Path,
dst: &Path,
force: bool,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
#[cfg(any(feature = "cli", feature = "files"))]
let src_owned = abs_path(src)?;
#[cfg(any(feature = "cli", feature = "files"))]
let dst_owned = abs_path(dst)?;
#[cfg(any(feature = "cli", feature = "files"))]
let src = src_owned.as_path();
#[cfg(any(feature = "cli", feature = "files"))]
let dst = dst_owned.as_path();
let op = Operation::FileRename {
from: src.to_string_lossy().into(),
to: dst.to_string_lossy().into(),
force,
};
let dest_str = Some(dst.to_string_lossy().to_string());
file_write_cross(op, src, mode, guard, "rename", dest_str)
}
pub fn file_append(
path: &Path,
content: &str,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
#[cfg(any(feature = "cli", feature = "files"))]
let path_owned = abs_path(path)?;
#[cfg(any(feature = "cli", feature = "files"))]
let path = path_owned.as_path();
let op = Operation::FileAppend {
path: path.to_string_lossy().into(),
content: content.into(),
};
file_write(op, path, mode, guard, "append")
}
pub fn file_prepend(
path: &Path,
content: &str,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
#[cfg(any(feature = "cli", feature = "files"))]
let path_owned = abs_path(path)?;
#[cfg(any(feature = "cli", feature = "files"))]
let path = path_owned.as_path();
let op = Operation::FilePrepend {
path: path.to_string_lossy().into(),
content: content.into(),
};
file_write(op, path, mode, guard, "prepend")
}