#![allow(unsafe_code)]
use std::ffi::OsString;
use std::fs::File;
use std::io::{Read, Seek, Write};
use std::net::Shutdown;
use std::os::unix::{fs::OpenOptionsExt, net::UnixStream, process::ExitStatusExt};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{io, process};
use crate::common::SudoPath;
use crate::exec::ExitReason;
use crate::log::{user_error, user_info};
use crate::system::file::{FileLock, create_temporary_dir};
use crate::system::wait::{Wait, WaitError, WaitOptions};
use crate::system::{ForkResult, fork, mark_fds_as_cloexec};
struct ParentFileInfo<'a> {
path: &'a Path,
file: File,
lock: FileLock,
old_data: Vec<u8>,
new_data_rx: UnixStream,
new_data: Option<Vec<u8>>,
}
struct ChildFileInfo<'a> {
path: &'a Path,
old_data: Vec<u8>,
tempfile_path: Option<PathBuf>,
new_data_tx: UnixStream,
}
pub(super) fn edit_files(
editor: (PathBuf, Vec<OsString>),
selected_files: Vec<(&SudoPath, File)>,
) -> io::Result<ExitReason> {
let mut files = vec![];
let mut child_files = vec![];
for (path, mut file) in selected_files {
let metadata = file.metadata().map_err(|e| {
io::Error::new(
e.kind(),
xlat!(
"failed to read metadata for {path}: {error}",
path = path.display(),
error = e
),
)
})?;
if !metadata.is_file() {
return Err(io::Error::other(xlat!(
"file {path} is not a regular file",
path = path.display()
)));
}
let lock = FileLock::exclusive(&file, true).map_err(|e| {
io::Error::new(
e.kind(),
xlat!(
"failed to lock {path}: {error}",
path = path.display(),
error = e
),
)
})?;
let mut old_data = Vec::new();
file.read_to_end(&mut old_data).map_err(|e| {
io::Error::new(
e.kind(),
xlat!(
"failed to read {path}: {error}",
path = path.display(),
error = e
),
)
})?;
let (parent_socket, child_socket) = UnixStream::pair()?;
files.push(ParentFileInfo {
path,
file,
lock,
old_data: old_data.clone(),
new_data_rx: parent_socket,
new_data: None,
});
child_files.push(ChildFileInfo {
path,
old_data,
tempfile_path: None,
new_data_tx: child_socket,
});
}
let ForkResult::Parent(command_pid) = unsafe { fork() }? else {
drop(files);
handle_child(editor, child_files)
};
drop(child_files);
for file in &mut files {
file.new_data =
Some(read_stream(&mut file.new_data_rx).map_err(|e| {
io::Error::new(e.kind(), format!("Failed to read from socket: {e}"))
})?);
}
let status = loop {
match command_pid.wait(WaitOptions::new()) {
Ok((_, status)) => break status,
Err(WaitError::Io(err)) if err.kind() == io::ErrorKind::Interrupted => {}
Err(err) => panic!("{err:?}"),
}
};
assert!(status.did_exit());
if let Some(signal) = status.term_signal() {
return Ok(ExitReason::Signal(signal));
} else if let Some(code) = status.exit_status() {
if code != 0 {
return Ok(ExitReason::Code(code));
}
} else {
return Ok(ExitReason::Code(1));
}
for mut file in files {
let data = file.new_data.expect("filled in above");
if data == file.old_data {
user_info!("{path} unchanged", path = file.path.display());
continue;
}
(move || {
file.file.rewind()?;
file.file.write_all(&data)?;
file.file.set_len(
data.len()
.try_into()
.expect("more than 18 exabyte of data???"),
)
})()
.map_err(|e| {
io::Error::new(
e.kind(),
xlat!(
"failed to write {path}: {error}",
path = file.path.display(),
error = e
),
)
})?;
drop(file.lock);
}
Ok(ExitReason::Code(0))
}
struct TempDirDropGuard(PathBuf);
impl Drop for TempDirDropGuard {
fn drop(&mut self) {
if let Err(e) = std::fs::remove_dir_all(&self.0) {
user_error!(
"failed to remove temporary directory {path}: {error}",
path = self.0.display(),
error = e
);
};
}
}
fn handle_child(editor: (PathBuf, Vec<OsString>), file: Vec<ChildFileInfo<'_>>) -> ! {
match handle_child_inner(editor, file) {
Ok(()) => process::exit(0),
Err(err) => {
user_error!("{error}", error = err);
process::exit(1);
}
}
}
fn handle_child_inner(
editor: (PathBuf, Vec<OsString>),
mut files: Vec<ChildFileInfo<'_>>,
) -> Result<(), String> {
mark_fds_as_cloexec().map_err(|e| format!("Failed to mark fds as CLOEXEC: {e}"))?;
unsafe {
libc::setuid(libc::getuid());
}
let tempdir = TempDirDropGuard(
create_temporary_dir()
.map_err(|e| xlat!("failed to create temporary directory: {error}", error = e))?,
);
for (i, file) in files.iter_mut().enumerate() {
let dir = tempdir.0.join(format!("{i}"));
std::fs::create_dir(&dir).map_err(|e| {
xlat!(
"failed to create temporary directory {path}: {error}",
path = dir.display(),
error = e
)
})?;
let tempfile_path = dir.join(file.path.file_name().expect("file must have filename"));
let mut tempfile = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.mode(0o600)
.open(&tempfile_path)
.map_err(|e| {
xlat!(
"failed to create temporary file {path}: {error}",
path = tempfile_path.display(),
error = e
)
})?;
tempfile.write_all(&file.old_data).map_err(|e| {
xlat!(
"failed to write to temporary file {path}: {error}",
path = tempfile_path.display(),
error = e
)
})?;
drop(tempfile);
file.tempfile_path = Some(tempfile_path);
}
let status = Command::new(&editor.0)
.args(editor.1)
.args(
files
.iter()
.map(|file| file.tempfile_path.as_ref().expect("filled in above")),
)
.status()
.map_err(|e| {
xlat!(
"failed to run editor {path}: {error}",
path = editor.0.display(),
error = e
)
})?;
if !status.success() {
drop(tempdir);
if let Some(signal) = status.signal() {
process::exit(128 + signal);
}
process::exit(status.code().unwrap_or(1));
}
for mut file in files {
let tempfile_path = file.tempfile_path.as_ref().expect("filled in above");
let new_data = std::fs::read(tempfile_path).map_err(|e| {
xlat!(
"failed to read from temporary file {path}: {error}",
path = tempfile_path.display(),
error = e
)
})?;
std::fs::remove_file(tempfile_path).map_err(|e| {
xlat!(
"failed to remove temporary file {path}: {error}",
path = tempfile_path.display(),
error = e
)
})?;
if new_data.is_empty() && new_data != file.old_data {
let answers = xlat!("yn");
match crate::visudo::ask_response(
&xlat!(
"sudoedit: truncate {path} to zero? (y/n) [n] ",
path = file.path.display()
),
answers,
answers
.chars()
.last()
.expect("translation files are corrupted"),
) {
Ok(val) if answers.starts_with(val) => {}
Ok('y') if !answers.contains('y') => {}
_ => {
user_info!("not overwriting {path}", path = file.path.display());
write_stream(&mut file.new_data_tx, &file.old_data)
.map_err(|e| xlat!("failed to write data to parent: {error}", error = e))?;
continue;
}
}
}
write_stream(&mut file.new_data_tx, &new_data)
.map_err(|e| xlat!("failed to write data to parent: {error}", error = e))?;
}
process::exit(0);
}
fn write_stream(socket: &mut UnixStream, data: &[u8]) -> io::Result<()> {
socket.write_all(data)?;
socket.shutdown(Shutdown::Both)?;
Ok(())
}
fn read_stream(socket: &mut UnixStream) -> io::Result<Vec<u8>> {
let mut new_data = Vec::new();
socket.read_to_end(&mut new_data)?;
Ok(new_data)
}