fix tests

This commit is contained in:
kirawi 2024-11-17 00:47:31 -05:00 committed by Mason Mac
parent 1329572d8d
commit be4ba5d04d
8 changed files with 161 additions and 228 deletions

1
Cargo.lock generated
View File

@ -1441,6 +1441,7 @@ dependencies = [
"chardetng",
"clipboard-win",
"crossterm",
"filetime",
"futures-util",
"helix-core",
"helix-dap",

View File

@ -1,6 +1,5 @@
//! From <https://github.com/Freaky/faccess>
use std::fs::File;
use std::io;
use std::path::Path;
@ -25,10 +24,7 @@ pub struct AccessMode: u8 {
mod imp {
use super::*;
use rustix::{
fd::AsFd,
fs::{Access, OpenOptionsExt},
};
use rustix::fs::Access;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
pub fn access(p: &Path, mode: AccessMode) -> io::Result<()> {
@ -76,24 +72,6 @@ pub fn hardlink_count(p: &Path) -> std::io::Result<u64> {
let metadata = p.metadata()?;
Ok(metadata.nlink())
}
pub fn create_copy_mode(from: &Path, to: &Path) -> io::Result<File> {
let from_meta = std::fs::metadata(from)?;
let mode = from_meta.permissions().mode();
let file = std::fs::OpenOptions::new()
.mode(mode)
.read(true)
.write(true)
.create_new(true)
.open(to)?;
// Change ownership
let from_meta = std::fs::metadata(from)?;
let uid = from_meta.uid();
let gid = from_meta.gid();
fchown(file.as_fd(), Some(uid), Some(gid))?;
Ok(file)
}
}
#[cfg(windows)]
@ -125,6 +103,7 @@ mod imp {
use std::ffi::c_void;
use std::fs::File;
use std::os::windows::{ffi::OsStrExt, fs::OpenOptionsExt, io::AsRawHandle};
// Licensed under MIT from faccess
@ -460,26 +439,6 @@ pub fn hardlink_count(p: &Path) -> io::Result<u64> {
let n = file_info(p)?.nNumberOfLinks as u64;
Ok(n)
}
pub fn create_copy_mode(from: &Path, to: &Path) -> io::Result<File> {
let sd = SecurityDescriptor::for_path(from)?;
// read/write still need to be set to true or `create_new` returns an error
let to_file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.access_mode(GENERIC_READ | GENERIC_WRITE | WRITE_OWNER | WRITE_DAC)
.create_new(true)
.open(to)?;
// Necessary because `security_attributes` is not exposed: https://github.com/rust-lang/libs-team/issues/314
chown(to_file.as_raw_handle(), sd)?;
let meta = std::fs::metadata(from)?;
let perms = meta.permissions();
std::fs::set_permissions(to, perms)?;
Ok(to_file)
}
}
// Licensed under MIT from faccess except for `copy_metadata`
@ -523,11 +482,6 @@ pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> {
imp::copy_metadata(from, to)
}
// /// Create a file copying permissions, uid, and gid of `from` at the the target destination `to`
// pub fn create_copy_mode(from: &Path, to: &Path) -> io::Result<File> {
// imp::create_copy_mode(from, to)
// }
#[cfg(windows)]
pub fn copy_ownership(from: &Path, to: &Path) -> io::Result<()> {
imp::copy_ownership(from, to)
@ -556,30 +510,33 @@ pub fn copy_xattr(from: &Path, to: &Path) -> io::Result<()> {
len => len,
};
let mut buf = vec![0i8; size];
let read = rustix::fs::listxattr(from, &mut buf)?;
fn i8_to_u8_slice(input: &[i8]) -> &[u8] {
// SAFETY: Simply reinterprets bytes
unsafe { std::slice::from_raw_parts(input.as_ptr() as *const u8, input.len()) }
}
let mut buf = vec![0; size];
let read = rustix::fs::listxattr(from, buf.as_mut_slice())?;
// Iterate over null-terminated C-style strings
// Two loops to avoid multiple allocations
// Find max-size for attributes
let mut max_attr_len = 0;
for attr_byte in buf.split(|&b| b == 0) {
let name = std::str::from_utf8(i8_to_u8_slice(attr_byte))
for attr_byte in buf[..read].split(|&b| b == 0) {
// handle platforms where c_char is i8
#[allow(clippy::unnecessary_cast)]
let conv =
unsafe { std::slice::from_raw_parts(attr_byte.as_ptr() as *const u8, attr_byte.len()) };
let name = std::str::from_utf8(conv)
.map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidData))?;
let attr_len = rustix::fs::getxattr(from, name, &mut [])?;
max_attr_len = max_attr_len.max(attr_len);
}
let mut attr_buf = vec![0u8; max_attr_len];
for attr_byte in buf.split(|&b| b == 0) {
let name = std::str::from_utf8(i8_to_u8_slice(attr_byte))
for attr_byte in buf[..read].split(|&b| b == 0) {
// handle platforms where c_char is i8
#[allow(clippy::unnecessary_cast)]
let conv =
unsafe { std::slice::from_raw_parts(attr_byte.as_ptr() as *const u8, attr_byte.len()) };
let name = std::str::from_utf8(conv)
.map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidData))?;
let read = rustix::fs::getxattr(from, name, &mut attr_buf)?;
let read = rustix::fs::getxattr(from, name, attr_buf.as_mut_slice())?;
// If we can't set xattr because it already exists, try to replace it
if read != 0 {
@ -599,69 +556,3 @@ fn i8_to_u8_slice(input: &[i8]) -> &[u8] {
Ok(())
}
/*
Neovim backup path function:
- If a backup is desired (would be disabled by a user if using a file watcher):
- Checks if user explicitly requested a copy
- Or automatically choose whether to copy or rename
- Offers options for:
- Breaking symlinks or hardlinks (not offered in Helix)
- Offers the ability to have a list of directories where the backup file is written:
- Default is: ".,$XDG_STATE_HOME/nvim/backup//"
- Offers ability to set backup extension
- For copy backup:
- If the file is a link, then the backup will have the name of the link
- Auto backup:
- Immediately set copy if:
- Is hardlink or symlink
- Then, tries to:
- Create a temporary file with the same permissions as the file to test if its ok to rename later
- If it fails, then set copy
- fchown created file
- If it fails or perms weren't copied, then set copy
- Delete test file
- Otherwise, will rename
- Break symlink/hardlink if requested
- Copy backup:
- If there is an error while creating the file, it will be propogated unless force write is true
- Try to create backup path in bdir:
- Tries first directory where this is possible
- If no directory exists, the last directory is created
- Filename is escaped and extension applied
- Check if backup already exists:
- Check if pre-existing file is a symlink to the original file (and don't attempt to create one)
- Dunno what p_bk is, but if false, it tries to create a different backup file path where each character before the extension is changed (if all attempts fail, then error)
- Copies file with UV_FS_COPYFILE_FICLONE
- Sets perm as os_setperm(*backupp, perm & 0777);
- On Unix:
- Attempts to set gid via chown:
- os_setperm(*backupp, (perm & 0707) | ((perm & 07) << 3) if fails
- Sets file time:
os_file_settime(*backupp,
(double)file_info_old->stat.st_atim.tv_sec,
(double)file_info_old->stat.st_mtim.tv_sec);
- On Windows, sets ACL
- Attempts to copy xattr if exists
- Rename backup:
- Backup is created by renaming original file:
- Don't if file is read-only and cpoptions has "W" flag
- Tries to find backup file name w/ bdir (similar to copy)
- Checks if a file with that name already exists:
- Attempts same method as copy backup to create a different filename
Neovim write:
- On Unix:
- If using :w! and file was read-only, make it writable (if process uid is same as file):
- Reset read-only flag if overwriting
- Executes fsync (will not propogate error if storage does not support op)
- Copies xattr for non-copy backups
- If a rename backup is being performed:
- Check if uid and gid are same as original file, and set if they aren't
- Set perms
- Either way, copy perms from old file to new file
- Either way, if not a backup copy, also set ACL (method seems to not do anything?)
- On failure:
- If a copy, copy contents from copy to original file
- Otherwise, rename backup back to original path
*/

View File

@ -224,7 +224,7 @@ fn path_from_bytes(slice: &[u8]) -> Result<PathBuf, Utf8Error> {
fn is_sep_byte(b: u8) -> bool {
if cfg!(windows) {
b == b'/' || b == b'\\'
b == b'/' || b == b'\\' || b == b':'
} else {
b == b'/'
}
@ -233,7 +233,7 @@ fn is_sep_byte(b: u8) -> bool {
/// Replaces all path separators in a path with %
pub fn escape_path(path: &Path) -> PathBuf {
let s = path.as_os_str().to_os_string();
let mut bytes = os_str_as_bytes(&s);
let mut bytes = os_str_as_bytes(s);
for b in bytes.iter_mut() {
if is_sep_byte(*b) {
*b = b'%';

View File

@ -234,6 +234,14 @@ pub fn new(args: Args, config: Config, lang_loader: syntax::Loader) -> Result<Se
.unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
}
let bck_config = &config.load().editor.backup;
if bck_config.kind != helix_view::editor::BackupKind::None
&& !bck_config.directories.iter().any(|p| p.exists())
{
// Initialize last directory for backups
std::fs::create_dir_all(bck_config.directories.last().unwrap())?;
}
editor.set_theme(theme);
#[cfg(windows)]

View File

@ -539,6 +539,7 @@ async fn test_symlink_write() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let mut file = tempfile::NamedTempFile::new_in(&dir)?;
// NOTE: This will fail on Windows unless ran in administrator
let symlink_path = dir.path().join("linked");
symlink(file.path(), &symlink_path)?;
@ -578,6 +579,7 @@ async fn test_symlink_write_fail() -> anyhow::Result<()> {
let file = helpers::new_readonly_tempfile_in_dir(&dir)?;
let symlink_path = dir.path().join("linked");
// NOTE: This will fail on Windows unless ran in administrator
symlink(file.path(), &symlink_path)?;
let mut app = helpers::AppBuilder::new()
@ -622,6 +624,7 @@ async fn test_symlink_write_relative() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new_in(&inner_dir)?;
let symlink_path = dir.path().join("linked");
let relative_path = std::path::PathBuf::from("b").join(file.path().file_name().unwrap());
// NOTE: This will fail on Windows unless ran in administrator
symlink(relative_path, &symlink_path)?;
let mut app = helpers::AppBuilder::new()

View File

@ -30,6 +30,7 @@ crossterm = { version = "0.28", optional = true }
tempfile = "3.14"
same-file = "1.0.1"
filetime = "0.2"
# Conversion traits
once_cell = "1.20"

View File

@ -1,6 +1,7 @@
use anyhow::{anyhow, bail, Error};
use arc_swap::access::{Access, DynAccess};
use arc_swap::access::DynAccess;
use arc_swap::ArcSwap;
use filetime::FileTime;
use futures_util::future::BoxFuture;
use futures_util::FutureExt;
use helix_core::auto_pairs::AutoPairs;
@ -23,7 +24,6 @@
use std::fmt::Display;
use std::future::Future;
use std::io;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::{Arc, Weak};
@ -153,7 +153,6 @@ async fn from(p: PathBuf, config: &BackupConfig) -> Result<Self, Error> {
// - it is a hardlink
// - it is a symlink
// - we don't have file create perms for the dir
// TODO: also set copy when perms can't be set or metadata can't be read
if !copy {
// Conservatively assume it is a hardlink if we can't read metadata
let is_hardlink = {
@ -165,16 +164,16 @@ async fn from(p: PathBuf, config: &BackupConfig) -> Result<Self, Error> {
copy = true;
} else {
// Check if we have write permissions by creating a temporary file
let from_meta = tokio::fs::metadata(&p).await?;
let perms = from_meta.permissions();
let mut builder = tempfile::Builder::new();
// TODO: Need a way to create cross-platform perms with more granularity
// builder.permissions()
builder.permissions(perms);
if let Ok(f) = builder.tempfile() {
// Check if we have perms to set perms
#[cfg(unix)]
{
use std::os::{fd::AsFd, unix::fs::MetadataExt};
let from_meta = tokio::fs::metadata(&p).await?;
let to_meta = tokio::fs::metadata(&f.path()).await?;
let _ = fchown(
f.as_file().as_fd(),
@ -205,12 +204,14 @@ async fn from(p: PathBuf, config: &BackupConfig) -> Result<Self, Error> {
// - directory is not writable
// - path is a directory
// - path exists
let mut dir_exists = false;
let escaped_p = helix_stdx::path::escape_path(&p);
// `.join` on absolute path replaces instead of append
debug_assert!(escaped_p.is_relative());
'outer: for dir in config.directories.iter().filter(|p| p.is_dir()) {
let ext = config.extension.as_str();
let bck_base_path = &dir.join(&escaped_p);
let mut backup = bck_base_path.join(&ext);
let mut backup = bck_base_path.with_extension(&ext);
// NOTE: Should we just overwrite regardless?
// If the backup file already exists, we'll try to add a number before the extension
@ -218,10 +219,11 @@ async fn from(p: PathBuf, config: &BackupConfig) -> Result<Self, Error> {
// NOTE: u8 since if we need more than 256, there might be an issue
let mut n: u8 = 1;
while backup.exists() {
backup = bck_base_path.join(n.to_string()).join(&ext);
let Some(n) = n.checked_add(1) else {
backup = bck_base_path.with_extension(format!("{n}.{ext}"));
if n == u8::MAX {
continue 'outer;
};
}
n = n + 1;
}
if copy {
@ -233,7 +235,7 @@ async fn from(p: PathBuf, config: &BackupConfig) -> Result<Self, Error> {
{
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let mut from_meta = tokio::fs::metadata(&p).await?;
let from_meta = tokio::fs::metadata(&p).await?;
let mut perms = from_meta.permissions();
// Strip s-bit
@ -249,7 +251,11 @@ async fn from(p: PathBuf, config: &BackupConfig) -> Result<Self, Error> {
perms.set_mode(new_perms);
}
std::fs::set_permissions(&backup, perms)?;
// TODO: Set time
let atime = FileTime::from_last_access_time(&from_meta);
let mtime = FileTime::from_last_modification_time(&from_meta);
filetime::set_file_times(&backup, atime, mtime)?;
copy_xattr(&p, &backup)?;
}
@ -260,25 +266,15 @@ async fn from(p: PathBuf, config: &BackupConfig) -> Result<Self, Error> {
copy_ownership(&p, &backup_)?;
Ok(())
})
.await?;
.await??;
}
return Ok(Self {
copy: true,
path: backup,
});
} else {
tokio::fs::rename(p, &backup).await?;
return Ok(Self {
copy: false,
path: backup,
});
}
return Ok(Self { copy, path: backup });
}
// TODO: Try to initialize last dir if none of the dirs exist
// TODO
bail!("err");
bail!("Could not write into a backup directory");
}
}
@ -1070,19 +1066,8 @@ impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send
}
}
}
let write_path = tokio::fs::read_link(&path)
.await
.ok()
.and_then(|p| {
if p.is_relative() {
path.parent().map(|parent| parent.join(p))
} else {
Some(p)
}
})
.unwrap_or_else(|| path.clone());
if readonly(&write_path) {
if readonly(&path) {
bail!(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"Path is read only"
@ -1090,61 +1075,72 @@ impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send
}
// Use a backup file
let meta = tokio::fs::metadata(&path).await?;
let mut bck = None;
let write_result = if bck_config.kind != crate::editor::BackupKind::None {
bck = Some(Backup::from(write_path.clone(), &bck_config).await?);
let bck = bck.unwrap();
// SECURITY: Ensure that the created file has the same perms as the original file
let dst = if !bck.copy {
let from_meta = tokio::fs::metadata(&write_path).await?;
let mut open_opt = tokio::fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = from_meta.permissions().mode();
open_opt.mode(mode);
let write_result: Result<(), Error> = async {
if path.exists() && bck_config.kind != crate::editor::BackupKind::None {
match Backup::from(path.clone(), &bck_config).await {
Ok(b) => bck = Some(b),
Err(_) if force => {}
Err(e) => bail!("Could not create backup: {e}"),
}
}
let file = open_opt.open(&bck.path).await?;
let to_meta = file.metadata().await?;
if let Some(ref bck) = bck {
// SECURITY: Ensure that the created file has the same perms as the original file
let mut dst = if !bck.copy {
let mut open_opt = tokio::fs::OpenOptions::new();
open_opt.read(true).write(true).create_new(true);
#[cfg(unix)]
{
// TODO: set gid/uid via fchown
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = meta.permissions().mode();
open_opt.mode(mode);
}
#[cfg(windows)]
{
let from = write_path.clone();
let to = bck.path.clone();
tokio::task::spawn_blocking(move || {
helix_stdx::faccess::copy_ownership(&from, &to)?;
Ok(())
})
.await?;
}
file
let file = open_opt.open(&path).await?;
#[cfg(unix)]
{
use std::os::fd::AsFd;
use std::os::unix::fs::MetadataExt;
helix_stdx::faccess::fchown(
file.as_fd(),
Some(meta.uid()),
Some(meta.gid()),
)?;
}
#[cfg(windows)]
{
let from = path.clone();
let to = bck.path.clone();
tokio::task::spawn_blocking(move || -> Result<(), Error> {
helix_stdx::faccess::copy_ownership(&from, &to)?;
Ok(())
})
.await??;
}
file
} else {
// SECURITY: Backup copy already exists
tokio::fs::File::create(&path).await?
};
to_writer(&mut dst, encoding_with_bom_info, &text).await?;
dst.sync_all().await?;
Ok(())
} else {
// SECURITY: Backup copy already exists
tokio::fs::File::create(&write_path).await?
};
let mut dst = tokio::fs::File::create(&path).await?;
to_writer(&mut dst, encoding_with_bom_info, &text).await?;
dst.sync_all().await?;
Ok(())
}
}
.await;
to_writer(&mut dst, encoding_with_bom_info, &text).await?;
dst.sync_all().await?;
Ok(())
} else {
let dst = tokio::fs::File::create(&write_path).await?;
to_writer(&mut dst, encoding_with_bom_info, &text).await?;
dst.sync_all().await?;
Ok(())
};
let save_time = match fs::metadata(&write_path).await {
let save_time = match fs::metadata(&path).await {
Ok(metadata) => metadata.modified().map_or(SystemTime::now(), |mtime| mtime),
Err(_) => SystemTime::now(),
};
@ -1158,35 +1154,41 @@ impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send
let mut delete_bck = true;
if write_result.is_err() {
// If original file no longer exists, then backup is renamed to original file
if !write_path.exists() {
if !tokio::fs::rename(&bck.path, &write_path)
if !path.exists() {
delete_bck = false;
if tokio::fs::rename(&bck.path, &path)
.await
.map_err(|e| {
delete_bck = false;
log::error!("Failed to restore backup on write failure: {e}")
})
.is_err()
.is_ok()
{
// TODO: Set timestamps to prior to write
// Reset timestamps
let atime = FileTime::from_last_access_time(&meta);
let mtime = FileTime::from_last_modification_time(&meta);
filetime::set_file_times(&path, atime, mtime)?;
}
} else {
if bck.copy {
// Restore backup from copy
let _ = tokio::fs::copy(&bck.path, &write_path).await.map_err(|e| {
let _ = tokio::fs::copy(&bck.path, &path).await.map_err(|e| {
delete_bck = false;
log::error!("Failed to restore backup on write failure: {e}")
});
} else {
// restore backup
let _ = tokio::fs::rename(&bck.path, &write_path)
.await
.map_err(|e| {
delete_bck = false;
log::error!("Failed to restore backup on write failure: {e}")
});
let _ = tokio::fs::rename(&bck.path, &path).await.map_err(|e| {
delete_bck = false;
log::error!("Failed to restore backup on write failure: {e}")
});
}
}
}
// Delete backup if we're done with it
if delete_bck {
tokio::fs::remove_file(bck.path).await?;
}
}
write_result?;

View File

@ -352,7 +352,9 @@ pub struct Config {
#[serde(rename_all = "kebab-case", default)]
pub struct BackupConfig {
pub kind: BackupKind,
#[serde(deserialize_with = "deserialize_non_empty_vec")]
pub directories: Vec<PathBuf>,
#[serde(deserialize_with = "deserialize_non_empty_str")]
pub extension: String,
}
@ -360,14 +362,39 @@ impl Default for BackupConfig {
fn default() -> Self {
Self {
kind: BackupKind::Auto,
// TODO: Prevent empty vector
directories: vec![helix_loader::state_dir().join("backup")],
// TODO: Prevent empty strings
extension: String::from("bck"),
}
}
}
fn deserialize_non_empty_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
use serde::de::Error;
let vec = Vec::<T>::deserialize(deserializer)?;
if vec.is_empty() {
return Err(<D::Error as Error>::custom("vector cannot be empty!"));
}
Ok(vec)
}
pub fn deserialize_non_empty_str<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let s = String::deserialize(deserializer)?;
if s.is_empty() {
return Err(<D::Error as Error>::custom("string cannot be empty"));
}
Ok(s)
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
#[serde(rename_all = "kebab-case")]
pub enum BackupKind {