mirror of
https://github.com/telemt/telemt.git
synced 2026-11-01 21:31:58 +03:00
205 lines
6.4 KiB
Rust
205 lines
6.4 KiB
Rust
use std::ffi::OsStr;
|
|
use std::io::{self, Read, Write};
|
|
use std::os::fd::{AsFd, OwnedFd};
|
|
use std::path::Path;
|
|
|
|
use nix::fcntl::{OFlag, openat, renameat};
|
|
use nix::sys::stat::Mode;
|
|
use nix::unistd::{UnlinkatFlags, fsync, unlinkat};
|
|
|
|
use super::path::{AnchoredPath, errno_to_io};
|
|
|
|
fn open_regular_at(anchored: &AnchoredPath, flags: OFlag, mode: u32) -> io::Result<std::fs::File> {
|
|
let descriptor = openat(
|
|
anchored.parent(),
|
|
anchored.name(),
|
|
flags | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC,
|
|
Mode::from_bits_truncate(mode),
|
|
)
|
|
.map_err(errno_to_io)?;
|
|
let file = std::fs::File::from(descriptor);
|
|
let metadata = file.metadata()?;
|
|
if !metadata.is_file() {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"target must be a regular file",
|
|
));
|
|
}
|
|
use std::os::unix::fs::MetadataExt;
|
|
if metadata.nlink() != 1 {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"target must have exactly one directory entry",
|
|
));
|
|
}
|
|
Ok(file)
|
|
}
|
|
|
|
/// Reads a regular file through an anchored parent with an allocation bound.
|
|
pub(crate) fn read_regular_limited(path: &Path, max_bytes: usize) -> io::Result<Vec<u8>> {
|
|
let anchored = AnchoredPath::open(path)?;
|
|
let mut file = open_regular_at(
|
|
&anchored,
|
|
OFlag::O_RDONLY | OFlag::O_NONBLOCK,
|
|
Mode::empty().bits(),
|
|
)?;
|
|
let before = file.metadata()?;
|
|
if before.len() > max_bytes as u64 {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"file exceeds configured size limit",
|
|
));
|
|
}
|
|
let mut bytes = Vec::with_capacity(before.len() as usize);
|
|
Read::take(&mut file, max_bytes.saturating_add(1) as u64).read_to_end(&mut bytes)?;
|
|
if bytes.len() > max_bytes {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"file exceeds configured size limit",
|
|
));
|
|
}
|
|
let after = file.metadata()?;
|
|
if !same_file_version(&before, &after) {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"file changed while it was read",
|
|
));
|
|
}
|
|
Ok(bytes)
|
|
}
|
|
|
|
/// Reads a bounded regular file on the blocking pool.
|
|
pub(crate) async fn read_regular_limited_async(
|
|
path: std::path::PathBuf,
|
|
max_bytes: usize,
|
|
) -> io::Result<Vec<u8>> {
|
|
tokio::task::spawn_blocking(move || read_regular_limited(&path, max_bytes))
|
|
.await
|
|
.map_err(|error| io::Error::other(format!("secure reader task failed: {error}")))?
|
|
}
|
|
|
|
fn same_file_version(left: &std::fs::Metadata, right: &std::fs::Metadata) -> bool {
|
|
use std::os::unix::fs::MetadataExt;
|
|
|
|
left.dev() == right.dev()
|
|
&& left.ino() == right.ino()
|
|
&& left.len() == right.len()
|
|
&& left.mtime() == right.mtime()
|
|
&& left.mtime_nsec() == right.mtime_nsec()
|
|
&& left.ctime() == right.ctime()
|
|
&& left.ctime_nsec() == right.ctime_nsec()
|
|
}
|
|
|
|
/// Opens an append-only regular file without following path components or hard links.
|
|
#[cfg(test)]
|
|
pub(crate) fn open_append_regular(path: &Path, mode: u32) -> io::Result<std::fs::File> {
|
|
let anchored = AnchoredPath::open_creating_parents(path, 0o750)?;
|
|
open_regular_at(
|
|
&anchored,
|
|
OFlag::O_WRONLY | OFlag::O_APPEND | OFlag::O_CREAT,
|
|
mode,
|
|
)
|
|
}
|
|
|
|
/// Opens one append-only file relative to an already anchored directory.
|
|
pub(crate) fn open_append_regular_at<Fd: AsFd>(
|
|
parent: Fd,
|
|
name: &OsStr,
|
|
mode: u32,
|
|
) -> io::Result<std::fs::File> {
|
|
if Path::new(name).components().count() != 1 {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"anchored file name must contain one component",
|
|
));
|
|
}
|
|
let descriptor = openat(
|
|
parent,
|
|
name,
|
|
OFlag::O_WRONLY | OFlag::O_APPEND | OFlag::O_CREAT | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC,
|
|
Mode::from_bits_truncate(mode),
|
|
)
|
|
.map_err(errno_to_io)?;
|
|
let file = std::fs::File::from(descriptor);
|
|
let metadata = file.metadata()?;
|
|
if !metadata.is_file() {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"target must be a regular file",
|
|
));
|
|
}
|
|
use std::os::unix::fs::MetadataExt;
|
|
if metadata.nlink() != 1 {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"target must have exactly one directory entry",
|
|
));
|
|
}
|
|
Ok(file)
|
|
}
|
|
|
|
/// Durably replaces a file through a same-directory descriptor-anchored rename.
|
|
pub(crate) fn atomic_replace(path: &Path, contents: &[u8], mode: u32) -> io::Result<()> {
|
|
let anchored = AnchoredPath::open_creating_parents(path, 0o750)?;
|
|
atomic_replace_anchored(&anchored, contents, mode)
|
|
}
|
|
|
|
/// Durably replaces a file on the blocking pool.
|
|
pub(crate) async fn atomic_replace_async(
|
|
path: std::path::PathBuf,
|
|
contents: Vec<u8>,
|
|
mode: u32,
|
|
) -> io::Result<()> {
|
|
tokio::task::spawn_blocking(move || atomic_replace(&path, &contents, mode))
|
|
.await
|
|
.map_err(|error| io::Error::other(format!("secure writer task failed: {error}")))?
|
|
}
|
|
|
|
fn atomic_replace_anchored(anchored: &AnchoredPath, contents: &[u8], mode: u32) -> io::Result<()> {
|
|
let temp_name = format!(".telemt.tmp-{}", rand::random::<u64>());
|
|
let descriptor = openat(
|
|
anchored.parent(),
|
|
temp_name.as_str(),
|
|
OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC,
|
|
Mode::from_bits_truncate(mode),
|
|
)
|
|
.map_err(errno_to_io)?;
|
|
let result = write_and_publish(descriptor, anchored, &temp_name, contents);
|
|
if result.is_err() {
|
|
let _ = unlinkat(
|
|
anchored.parent(),
|
|
temp_name.as_str(),
|
|
UnlinkatFlags::NoRemoveDir,
|
|
);
|
|
}
|
|
result
|
|
}
|
|
|
|
fn write_and_publish(
|
|
descriptor: OwnedFd,
|
|
anchored: &AnchoredPath,
|
|
temp_name: &str,
|
|
contents: &[u8],
|
|
) -> io::Result<()> {
|
|
let mut file = std::fs::File::from(descriptor);
|
|
file.write_all(contents)?;
|
|
file.sync_all()?;
|
|
renameat(
|
|
anchored.parent(),
|
|
temp_name,
|
|
anchored.parent(),
|
|
anchored.name(),
|
|
)
|
|
.map_err(errno_to_io)?;
|
|
fsync(anchored.parent()).map_err(errno_to_io)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(super) fn atomic_replace_after_anchor(
|
|
anchored: &AnchoredPath,
|
|
contents: &[u8],
|
|
mode: u32,
|
|
) -> io::Result<()> {
|
|
atomic_replace_anchored(anchored, contents, mode)
|
|
}
|