mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-27 13:23:09 +00:00
7b7d16da48
This was based on a misunderstanding. On musl, 64-bit time_t on 32-bit architectures was introduced in version 1.2.0, by introducing new symbols. The old symbols still exist, to allow programs compiled against older versions to keep running on 1.2.0+, preserving ABI-compatibility. (see musl commit 38143339646a4ccce8afe298c34467767c899f51) Programs compiled against 1.2.0+ will get the new symbols, and will therefore think time_t is 64-bit. Unfortunately, rust's libc crate uses its own definition of these types, and does not check for musl version. Currently, it includes the pre-1.2.0 32-bit type. That means: - If you run on a 32-bit system like i686 - ... and compile against a C-library other than libc - ... and pass it a time_t-containing struct like timespec or stat ... you need to arrange for that library to be built against musl <1.2.0. Or, as https://github.com/ericonr/rust-time64 says: > Therefore, for "old" 32-bit targets (riscv32 is supposed to default to time64), > any Rust code that interacts with C code built on musl after 1.2.0, > using types based on time_t (arguably, the main ones are struct timespec and struct stat) in their interface, > will be completely miscompiled. However, while fish runs on i686 and compiles against pcre2, we do not pass pcre2 a time_t. Our only uses of time_t are confined to interactions with libc, in which case with musl we would simply use the legacy ABI. I have compiled an i686 fish against musl to confirm and can find no issue. This reverts commit55196ee2a0
. This reverts commit4992f88966
. This reverts commit46c8ba2c9f
. This reverts commit3a9b4149da
. This reverts commit5f9e9cbe74
. This reverts commit338579b78c
. This reverts commitd19e5508d7
. This reverts commitb64045dc18
. Closes #10634
1033 lines
36 KiB
Rust
1033 lines
36 KiB
Rust
#![allow(clippy::bad_bit_mask)]
|
|
|
|
use crate::common::{
|
|
read_loop, str2wcstring, timef, unescape_string, valid_var_name, wcs2zstring, write_loop,
|
|
UnescapeFlags, UnescapeStringStyle,
|
|
};
|
|
use crate::env::{EnvVar, EnvVarFlags, VarTable};
|
|
use crate::fallback::fish_mkstemp_cloexec;
|
|
use crate::fds::{open_cloexec, wopen_cloexec};
|
|
use crate::flog::{FLOG, FLOGF};
|
|
use crate::path::path_get_config;
|
|
use crate::path::{path_get_config_remoteness, DirRemoteness};
|
|
use crate::wchar::{decode_byte_from_char, prelude::*};
|
|
use crate::wcstringutil::{join_strings, string_suffixes_string, LineIterator};
|
|
use crate::wutil::{
|
|
file_id_for_fd, file_id_for_path, file_id_for_path_narrow, wdirname, wrealpath, wrename, wstat,
|
|
wunlink, FileId, INVALID_FILE_ID,
|
|
};
|
|
use errno::{errno, Errno};
|
|
use libc::{EINTR, LOCK_EX};
|
|
use nix::{fcntl::OFlag, sys::stat::Mode};
|
|
use std::collections::hash_map::Entry;
|
|
use std::collections::HashSet;
|
|
use std::ffi::CString;
|
|
use std::fs::File;
|
|
use std::mem::MaybeUninit;
|
|
use std::os::fd::{AsFd, AsRawFd, RawFd};
|
|
use std::os::unix::prelude::MetadataExt;
|
|
|
|
// Pull in the O_EXLOCK constant if it is defined, otherwise set it to 0.
|
|
#[cfg(any(bsd, target_os = "macos"))]
|
|
const O_EXLOCK: OFlag = OFlag::O_EXLOCK;
|
|
|
|
#[cfg(not(any(bsd, target_os = "macos")))]
|
|
const O_EXLOCK: OFlag = OFlag::empty();
|
|
|
|
/// Callback data, reflecting a change in universal variables.
|
|
pub struct CallbackData {
|
|
// The name of the variable.
|
|
pub key: WString,
|
|
|
|
// The value of the variable, or none if it is erased.
|
|
pub val: Option<EnvVar>,
|
|
}
|
|
|
|
pub type CallbackDataList = Vec<CallbackData>;
|
|
|
|
// List of fish universal variable formats.
|
|
// This is exposed for testing.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum UvarFormat {
|
|
fish_2_x,
|
|
fish_3_0,
|
|
future,
|
|
}
|
|
|
|
/// Class representing universal variables.
|
|
pub struct EnvUniversal {
|
|
// Path that we save to. This is set in initialize(). If empty, initialize has not been called.
|
|
vars_path: WString,
|
|
narrow_vars_path: CString,
|
|
|
|
// The table of variables.
|
|
vars: VarTable,
|
|
|
|
// Keys that have been modified, and need to be written. A value here that is not present in
|
|
// vars indicates a deleted value.
|
|
modified: HashSet<WString>,
|
|
|
|
// A generation count which is incremented every time an exported variable is modified.
|
|
export_generation: u64,
|
|
|
|
// Whether it's OK to save. This may be set to false if we discover that a future version of
|
|
// fish wrote the uvars contents.
|
|
ok_to_save: bool,
|
|
|
|
// If true, attempt to flock the uvars file.
|
|
// This latches to false if the file is found to be remote, where flock may hang.
|
|
do_flock: bool,
|
|
|
|
// File id from which we last read.
|
|
last_read_file: FileId,
|
|
}
|
|
|
|
impl EnvUniversal {
|
|
// Construct an empty universal variables.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
vars_path: Default::default(),
|
|
narrow_vars_path: Default::default(),
|
|
vars: Default::default(),
|
|
modified: Default::default(),
|
|
export_generation: 1,
|
|
ok_to_save: true,
|
|
do_flock: true,
|
|
last_read_file: INVALID_FILE_ID,
|
|
}
|
|
}
|
|
// Get the value of the variable with the specified name.
|
|
pub fn get(&self, name: &wstr) -> Option<EnvVar> {
|
|
self.vars.get(name).cloned()
|
|
}
|
|
// Return flags from the variable with the given name.
|
|
pub fn get_flags(&self, name: &wstr) -> Option<EnvVarFlags> {
|
|
self.vars.get(name).map(|var| var.get_flags())
|
|
}
|
|
// Sets a variable.
|
|
pub fn set(&mut self, key: &wstr, var: EnvVar) {
|
|
let exports = var.exports();
|
|
match self.vars.entry(key.to_owned()) {
|
|
Entry::Occupied(mut entry) => {
|
|
if entry.get() == &var {
|
|
return;
|
|
}
|
|
entry.insert(var);
|
|
}
|
|
Entry::Vacant(entry) => {
|
|
entry.insert(var);
|
|
}
|
|
};
|
|
self.modified.insert(key.to_owned());
|
|
if exports {
|
|
self.export_generation += 1;
|
|
}
|
|
}
|
|
// Removes a variable. Returns true if it was found, false if not.
|
|
pub fn remove(&mut self, key: &wstr) -> bool {
|
|
if let Some(var) = self.vars.remove(key) {
|
|
if var.exports() {
|
|
self.export_generation += 1;
|
|
}
|
|
self.modified.insert(key.to_owned());
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
// Gets variable names.
|
|
pub fn get_names(&self, show_exported: bool, show_unexported: bool) -> Vec<WString> {
|
|
let mut result = vec![];
|
|
for (key, var) in &self.vars {
|
|
if (var.exports() && show_exported) || (!var.exports() && show_unexported) {
|
|
result.push(key.clone());
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
/// Get a view on the universal variable table.
|
|
pub fn get_table(&self) -> &VarTable {
|
|
&self.vars
|
|
}
|
|
|
|
/// Initialize this uvars for the default path.
|
|
/// This should be called at most once on any given instance.
|
|
pub fn initialize(&mut self, callbacks: &mut CallbackDataList) {
|
|
// Set do_flock to false immediately if the default variable path is on a remote filesystem.
|
|
// See #7968.
|
|
if path_get_config_remoteness() == DirRemoteness::remote {
|
|
self.do_flock = false;
|
|
}
|
|
self.initialize_at_path(callbacks, default_vars_path());
|
|
}
|
|
|
|
/// Initialize a this uvars for a given path.
|
|
/// This is exposed for testing only.
|
|
pub fn initialize_at_path(&mut self, callbacks: &mut CallbackDataList, path: WString) {
|
|
if path.is_empty() {
|
|
return;
|
|
}
|
|
assert!(!self.initialized(), "Already initialized");
|
|
self.vars_path = path;
|
|
|
|
if self.load_from_path(callbacks) {
|
|
// Successfully loaded from our normal path.
|
|
}
|
|
}
|
|
|
|
/// Reads and writes variables at the correct path. Returns true if modified variables were
|
|
/// written.
|
|
pub fn sync(&mut self, callbacks: &mut CallbackDataList) -> bool {
|
|
if !self.initialized() {
|
|
return false;
|
|
}
|
|
|
|
FLOG!(uvar_file, "universal log sync");
|
|
// Our saving strategy:
|
|
//
|
|
// 1. Open the file, producing an fd.
|
|
// 2. Lock the file (may be combined with step 1 on systems with O_EXLOCK)
|
|
// 3. After taking the lock, check if the file at the given path is different from what we
|
|
// opened. If so, start over.
|
|
// 4. Read from the file. This can be elided if its dev/inode is unchanged since the last read
|
|
// 5. Open an adjacent temporary file
|
|
// 6. Write our changes to an adjacent file
|
|
// 7. Move the adjacent file into place via rename. This is assumed to be atomic.
|
|
// 8. Release the lock and close the file
|
|
//
|
|
// Consider what happens if Process 1 and 2 both do this simultaneously. Can there be data loss?
|
|
// Process 1 opens the file and then attempts to take the lock. Now, either process 1 will see
|
|
// the original file, or process 2's new file. If it sees the new file, we're OK: it's going to
|
|
// read from the new file, and so there's no data loss. If it sees the old file, then process 2
|
|
// must have locked it (if process 1 locks it, switch their roles). The lock will block until
|
|
// process 2 reaches step 7; at that point process 1 will reach step 2, notice that the file has
|
|
// changed, and then start over.
|
|
//
|
|
// It's possible that the underlying filesystem does not support locks (lockless NFS). In this
|
|
// case, we risk data loss if two shells try to write their universal variables simultaneously.
|
|
// In practice this is unlikely, since uvars are usually written interactively.
|
|
//
|
|
// Prior versions of fish used a hard link scheme to support file locking on lockless NFS. The
|
|
// risk here is that if the process crashes or is killed while holding the lock, future
|
|
// instances of fish will not be able to obtain it. This seems to be a greater risk than that of
|
|
// data loss on lockless NFS. Users who put their home directory on lockless NFS are playing
|
|
// with fire anyways.
|
|
// If we have no changes, just load.
|
|
if self.modified.is_empty() {
|
|
self.load_from_path_narrow(callbacks);
|
|
FLOG!(uvar_file, "universal log no modifications");
|
|
return false;
|
|
}
|
|
|
|
let directory = wdirname(&self.vars_path).to_owned();
|
|
|
|
FLOG!(uvar_file, "universal log performing full sync");
|
|
|
|
// Open the file.
|
|
let Some(mut vars_file) = self.open_and_acquire_lock() else {
|
|
FLOG!(uvar_file, "universal log open_and_acquire_lock() failed");
|
|
return false;
|
|
};
|
|
|
|
// Read from it.
|
|
self.load_from_fd(&mut vars_file, callbacks);
|
|
|
|
if self.ok_to_save {
|
|
self.save(&directory)
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
|
|
/// Populate a variable table `out_vars` from a `s` string.
|
|
/// This is exposed for testing only.
|
|
/// Return the format of the file that we read.
|
|
pub fn populate_variables(s: &[u8], out_vars: &mut VarTable) -> UvarFormat {
|
|
// Decide on the format.
|
|
let format = Self::format_for_contents(s);
|
|
|
|
let iter = LineIterator::new(s);
|
|
let mut wide_line = WString::new();
|
|
let mut storage = WString::new();
|
|
for line in iter {
|
|
// Skip empties and constants.
|
|
if line.is_empty() || line[0] == b'#' {
|
|
continue;
|
|
}
|
|
|
|
// Convert to UTF8.
|
|
wide_line.clear();
|
|
let Ok(line) = std::str::from_utf8(line) else {
|
|
continue;
|
|
};
|
|
wide_line = WString::from_str(line);
|
|
|
|
match format {
|
|
UvarFormat::fish_2_x => {
|
|
Self::parse_message_2x_internal(&wide_line, out_vars, &mut storage);
|
|
}
|
|
UvarFormat::fish_3_0 => {
|
|
Self::parse_message_30_internal(&wide_line, out_vars, &mut storage);
|
|
}
|
|
// For future formats, just try with the most recent one.
|
|
UvarFormat::future => {
|
|
Self::parse_message_30_internal(&wide_line, out_vars, &mut storage);
|
|
}
|
|
}
|
|
}
|
|
format
|
|
}
|
|
|
|
/// Guess a file format. Exposed for testing only.
|
|
/// Return the format corresponding to file contents `s`.
|
|
pub fn format_for_contents(s: &[u8]) -> UvarFormat {
|
|
// Walk over leading comments, looking for one like '# version'
|
|
let iter = LineIterator::new(s);
|
|
for line in iter {
|
|
if line.is_empty() {
|
|
continue;
|
|
}
|
|
if line[0] != b'#' {
|
|
// Exhausted leading comments.
|
|
break;
|
|
}
|
|
// Note scanf %s is max characters to write; add 1 for null terminator.
|
|
let mut versionbuf: MaybeUninit<[u8; 64 + 1]> = MaybeUninit::uninit();
|
|
// Safety: test-only
|
|
let cstr = CString::new(line).unwrap();
|
|
if unsafe {
|
|
libc::sscanf(
|
|
cstr.as_ptr(),
|
|
b"# VERSION: %64s\0".as_ptr().cast(),
|
|
versionbuf.as_mut_ptr(),
|
|
)
|
|
} != 1
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Try reading the version.
|
|
let versionbuf = unsafe { versionbuf.assume_init() };
|
|
return if versionbuf.starts_with(UVARS_VERSION_3_0)
|
|
&& versionbuf[UVARS_VERSION_3_0.len()] == b'\0'
|
|
{
|
|
UvarFormat::fish_3_0
|
|
} else {
|
|
UvarFormat::future
|
|
};
|
|
}
|
|
// No version found, assume 2.x
|
|
return UvarFormat::fish_2_x;
|
|
}
|
|
|
|
/// Serialize a variable list. Exposed for testing only.
|
|
pub fn serialize_with_vars(vars: &VarTable) -> Vec<u8> {
|
|
let mut contents = vec![];
|
|
contents.extend_from_slice(SAVE_MSG);
|
|
contents.extend_from_slice(b"# VERSION: ");
|
|
contents.extend_from_slice(UVARS_VERSION_3_0);
|
|
contents.push(b'\n');
|
|
|
|
// Preserve legacy behavior by sorting the values first
|
|
let mut cloned: Vec<(&wstr, &EnvVar)> =
|
|
vars.iter().map(|(key, var)| (key.as_ref(), var)).collect();
|
|
cloned.sort_by(|(lkey, _), (rkey, _)| lkey.cmp(rkey));
|
|
|
|
for (key, var) in cloned {
|
|
// Append the entry. Note that append_file_entry may fail, but that only affects one
|
|
// variable; soldier on.
|
|
append_file_entry(
|
|
var.get_flags(),
|
|
key,
|
|
&encode_serialized(var.as_list()),
|
|
&mut contents,
|
|
);
|
|
}
|
|
contents
|
|
}
|
|
|
|
/// Exposed for testing only.
|
|
#[cfg(test)]
|
|
pub fn is_ok_to_save(&self) -> bool {
|
|
self.ok_to_save
|
|
}
|
|
|
|
/// Access the export generation.
|
|
pub fn get_export_generation(&self) -> u64 {
|
|
self.export_generation
|
|
}
|
|
|
|
/// Return whether we are initialized.
|
|
fn initialized(&self) -> bool {
|
|
!self.vars_path.is_empty()
|
|
}
|
|
|
|
fn load_from_path(&mut self, callbacks: &mut CallbackDataList) -> bool {
|
|
self.narrow_vars_path = wcs2zstring(&self.vars_path);
|
|
self.load_from_path_narrow(callbacks)
|
|
}
|
|
|
|
fn load_from_path_narrow(&mut self, callbacks: &mut CallbackDataList) -> bool {
|
|
// Check to see if the file is unchanged. We do this again in load_from_fd, but this avoids
|
|
// opening the file unnecessarily.
|
|
if self.last_read_file != INVALID_FILE_ID
|
|
&& file_id_for_path_narrow(&self.narrow_vars_path) == self.last_read_file
|
|
{
|
|
FLOG!(uvar_file, "universal log sync elided based on fast stat()");
|
|
return true;
|
|
}
|
|
|
|
let Ok(mut file) = open_cloexec(&self.narrow_vars_path, OFlag::O_RDONLY, Mode::empty())
|
|
else {
|
|
return false;
|
|
};
|
|
|
|
FLOG!(uvar_file, "universal log reading from file");
|
|
self.load_from_fd(&mut file, callbacks);
|
|
true
|
|
}
|
|
|
|
// Load environment variables from the opened [`File`] `file`. It must be mutable because we
|
|
// will read from the underlying fd.
|
|
fn load_from_fd(&mut self, file: &mut File, callbacks: &mut CallbackDataList) {
|
|
// Get the dev / inode.
|
|
let current_file = file_id_for_fd(file.as_fd());
|
|
if current_file == self.last_read_file {
|
|
FLOG!(uvar_file, "universal log sync elided based on fstat()");
|
|
} else {
|
|
// Read a variables table from the file.
|
|
let mut new_vars = VarTable::new();
|
|
let format = Self::read_message_internal(file.as_raw_fd(), &mut new_vars);
|
|
|
|
// Hacky: if the read format is in the future, avoid overwriting the file: never try to
|
|
// save.
|
|
if format == UvarFormat::future {
|
|
self.ok_to_save = false;
|
|
}
|
|
|
|
// Announce changes and update our exports generation.
|
|
self.generate_callbacks_and_update_exports(&new_vars, callbacks);
|
|
|
|
// Acquire the new variables.
|
|
self.acquire_variables(new_vars);
|
|
self.last_read_file = current_file;
|
|
}
|
|
}
|
|
|
|
// Functions concerned with saving.
|
|
fn open_and_acquire_lock(&mut self) -> Option<File> {
|
|
// Attempt to open the file for reading at the given path, atomically acquiring a lock. On BSD,
|
|
// we can use O_EXLOCK. On Linux, we open the file, take a lock, and then compare fstat() to
|
|
// stat(); if they match, it means that the file was not replaced before we acquired the lock.
|
|
//
|
|
// We pass O_RDONLY with O_CREAT; this creates a potentially empty file. We do this so that we
|
|
// have something to lock on.
|
|
let mut locked_by_open = false;
|
|
let mut flags = OFlag::O_RDWR | OFlag::O_CREAT;
|
|
|
|
if !O_EXLOCK.is_empty() && self.do_flock {
|
|
flags |= O_EXLOCK;
|
|
locked_by_open = true;
|
|
}
|
|
|
|
loop {
|
|
let mut file =
|
|
match wopen_cloexec(&self.vars_path, flags, Mode::from_bits_truncate(0o644)) {
|
|
Ok(file) => file,
|
|
Err(nix::Error::EINTR) => continue,
|
|
Err(err) => {
|
|
if !O_EXLOCK.is_empty() {
|
|
if flags.contains(O_EXLOCK)
|
|
&& [nix::Error::ENOTSUP, nix::Error::EOPNOTSUPP].contains(&err)
|
|
{
|
|
// Filesystem probably does not support locking. Give up on locking.
|
|
// Note that on Linux the two errno symbols have the same value but on BSD they're
|
|
// different.
|
|
flags &= !O_EXLOCK;
|
|
self.do_flock = false;
|
|
locked_by_open = false;
|
|
continue;
|
|
}
|
|
}
|
|
FLOG!(
|
|
error,
|
|
wgettext_fmt!(
|
|
"Unable to open universal variable file '%s': %s",
|
|
&self.vars_path,
|
|
err.to_string()
|
|
)
|
|
);
|
|
return None;
|
|
}
|
|
};
|
|
|
|
// Lock if we want to lock and open() didn't do it for us.
|
|
// If flock fails, give up on locking forever.
|
|
if self.do_flock && !locked_by_open {
|
|
if !flock_uvar_file(&mut file) {
|
|
self.do_flock = false;
|
|
}
|
|
}
|
|
|
|
// Hopefully we got the lock. However, it's possible the file changed out from under us
|
|
// while we were waiting for the lock. Make sure that didn't happen.
|
|
if file_id_for_fd(file.as_fd()) != file_id_for_path(&self.vars_path) {
|
|
// Oops, it changed! Try again.
|
|
drop(file);
|
|
} else {
|
|
return Some(file);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn open_temporary_file(
|
|
&mut self,
|
|
directory: &wstr,
|
|
out_path: &mut WString,
|
|
) -> Result<File, Errno> {
|
|
// Create and open a temporary file for writing within the given directory. Try to create a
|
|
// temporary file, up to 10 times. We don't use mkstemps because we want to open it CLO_EXEC.
|
|
// This should almost always succeed on the first try.
|
|
assert!(!string_suffixes_string(L!("/"), directory));
|
|
|
|
let mut attempt = 0;
|
|
let tmp_name_template = directory.to_owned() + L!("/fishd.tmp.XXXXXX");
|
|
let result = loop {
|
|
attempt += 1;
|
|
let result = fish_mkstemp_cloexec(wcs2zstring(&tmp_name_template));
|
|
match (result, attempt) {
|
|
(Ok(r), _) => break r,
|
|
(Err(e), 10) => {
|
|
FLOG!(
|
|
error,
|
|
// We previously used to log a copy of the buffer we expected mk(o)stemp to
|
|
// update with the new path, but mkstemp(3) says the contents of the buffer
|
|
// are undefined in case of EEXIST, but left unchanged in case of EINVAL. So
|
|
// just log the original template we pass in to the function instead.
|
|
wgettext_fmt!(
|
|
"Unable to create temporary file '%ls': %s",
|
|
&tmp_name_template,
|
|
e.to_string()
|
|
)
|
|
);
|
|
return Err(e);
|
|
}
|
|
_ => continue,
|
|
}
|
|
};
|
|
|
|
*out_path = str2wcstring(result.1.as_bytes());
|
|
Ok(result.0)
|
|
}
|
|
|
|
/// Writes our state to the fd. path is provided only for error reporting.
|
|
fn write_to_fd(&mut self, fd: impl AsFd, path: &wstr) -> std::io::Result<usize> {
|
|
let fd = fd.as_fd();
|
|
let contents = Self::serialize_with_vars(&self.vars);
|
|
|
|
let res = write_loop(&fd, &contents);
|
|
match res.as_ref() {
|
|
Ok(_) => {
|
|
// Since we just wrote out this file, it matches our internal state; pretend we read from it.
|
|
self.last_read_file = file_id_for_fd(fd);
|
|
}
|
|
Err(err) => {
|
|
let error = Errno(err.raw_os_error().unwrap());
|
|
FLOG!(
|
|
error,
|
|
wgettext_fmt!(
|
|
"Unable to write to universal variables file '%ls': %s",
|
|
path,
|
|
error.to_string()
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// We don't close the file.
|
|
res
|
|
}
|
|
|
|
fn move_new_vars_file_into_place(&mut self, src: &wstr, dst: &wstr) -> bool {
|
|
let ret = wrename(src, dst);
|
|
if ret != 0 {
|
|
let error = errno();
|
|
FLOG!(
|
|
error,
|
|
wgettext_fmt!(
|
|
"Unable to rename file from '%ls' to '%ls': %s",
|
|
src,
|
|
dst,
|
|
error.to_string()
|
|
)
|
|
);
|
|
}
|
|
ret == 0
|
|
}
|
|
|
|
// Given a variable table, generate callbacks representing the difference between our vars and
|
|
// the new vars. Also update our exports generation count as necessary.
|
|
fn generate_callbacks_and_update_exports(
|
|
&mut self,
|
|
new_vars: &VarTable,
|
|
callbacks: &mut CallbackDataList,
|
|
) {
|
|
// Construct callbacks for erased values.
|
|
for (key, value) in &self.vars {
|
|
// Skip modified values.
|
|
if self.modified.contains(key) {
|
|
continue;
|
|
}
|
|
|
|
// If the value is not present in new_vars, it has been erased.
|
|
if !new_vars.contains_key(key) {
|
|
callbacks.push(CallbackData {
|
|
key: key.clone(),
|
|
val: None,
|
|
});
|
|
if value.exports() {
|
|
self.export_generation += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Construct callbacks for newly inserted or changed values.
|
|
for (key, new_entry) in new_vars {
|
|
// Skip modified values.
|
|
if self.modified.contains(key) {
|
|
continue;
|
|
}
|
|
|
|
let existing = self.vars.get(key);
|
|
|
|
// See if the value has changed.
|
|
let old_exports = existing.map_or(false, |v| v.exports());
|
|
let export_changed = old_exports != new_entry.exports();
|
|
let value_changed = existing.map_or(false, |v| v != new_entry);
|
|
if export_changed || value_changed {
|
|
self.export_generation += 1;
|
|
}
|
|
if existing.is_none() || export_changed || value_changed {
|
|
// Value is set for the first time, or has changed.
|
|
callbacks.push(CallbackData {
|
|
key: key.clone(),
|
|
val: Some(new_entry.clone()),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Given a variable table, copy unmodified values into self.
|
|
fn acquire_variables(&mut self, mut vars_to_acquire: VarTable) {
|
|
// Copy modified values from existing vars to vars_to_acquire.
|
|
for key in &self.modified {
|
|
match self.vars.get(key) {
|
|
None => {
|
|
/* The value has been deleted. */
|
|
vars_to_acquire.remove(key);
|
|
}
|
|
Some(src) => {
|
|
// The value has been modified. Copy it over. Note we can destructively modify the
|
|
// source entry in vars since we are about to get rid of this->vars entirely.
|
|
vars_to_acquire.insert(key.clone(), src.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
// We have constructed all the callbacks and updated vars_to_acquire. Acquire it!
|
|
self.vars = vars_to_acquire;
|
|
}
|
|
|
|
fn populate_1_variable(
|
|
input: &wstr,
|
|
flags: EnvVarFlags,
|
|
vars: &mut VarTable,
|
|
storage: &mut WString,
|
|
) -> bool {
|
|
let s = skip_spaces(input);
|
|
let Some(colon) = s.chars().position(|c| c == ':') else {
|
|
return false;
|
|
};
|
|
|
|
// Parse out the value into storage, and decode it into a variable.
|
|
storage.clear();
|
|
let Some(unescaped) = unescape_string(
|
|
&s[colon + 1..],
|
|
UnescapeStringStyle::Script(UnescapeFlags::default()),
|
|
) else {
|
|
return false;
|
|
};
|
|
*storage = unescaped;
|
|
let var = EnvVar::new_vec(decode_serialized(&*storage), flags);
|
|
|
|
// Parse out the key and write into the map.
|
|
*storage = s[..colon].to_owned();
|
|
let key = &*storage;
|
|
(*vars).insert(key.clone(), var);
|
|
true
|
|
}
|
|
/// Parse message msg per fish 3.0 format.
|
|
fn parse_message_30_internal(msg: &wstr, vars: &mut VarTable, storage: &mut WString) {
|
|
use fish3_uvars as f3;
|
|
if msg.starts_with(L!("#")) {
|
|
return;
|
|
}
|
|
|
|
let mut cursor = msg;
|
|
if !r#match(&mut cursor, f3::SETUVAR) {
|
|
FLOGF!(warning, PARSE_ERR, msg);
|
|
return;
|
|
}
|
|
// Parse out flags.
|
|
let mut flags = EnvVarFlags::default();
|
|
loop {
|
|
cursor = skip_spaces(cursor);
|
|
if cursor.char_at(0) != '-' {
|
|
break;
|
|
}
|
|
if r#match(&mut cursor, f3::EXPORT) {
|
|
flags |= EnvVarFlags::EXPORT;
|
|
} else if r#match(&mut cursor, f3::PATH) {
|
|
flags |= EnvVarFlags::PATHVAR;
|
|
} else {
|
|
// Skip this unknown flag, for future proofing.
|
|
while !cursor.is_empty() && !matches!(cursor.char_at(0), ' ' | '\t') {
|
|
cursor = &cursor[1..];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Populate the variable with these flags.
|
|
if !Self::populate_1_variable(cursor, flags, vars, storage) {
|
|
FLOGF!(warning, PARSE_ERR, msg);
|
|
}
|
|
}
|
|
|
|
/// Parse message msg per fish 2.x format.
|
|
fn parse_message_2x_internal(msg: &wstr, vars: &mut VarTable, storage: &mut WString) {
|
|
use fish2x_uvars as f2x;
|
|
let mut cursor = msg;
|
|
|
|
if cursor.char_at(0) == '#' {
|
|
return;
|
|
}
|
|
let mut flags = EnvVarFlags::default();
|
|
if r#match(&mut cursor, f2x::SET_EXPORT) {
|
|
flags |= EnvVarFlags::EXPORT;
|
|
} else if r#match(&mut cursor, f2x::SET) {
|
|
} else {
|
|
FLOGF!(warning, PARSE_ERR, msg);
|
|
return;
|
|
}
|
|
|
|
if !Self::populate_1_variable(cursor, flags, vars, storage) {
|
|
FLOGF!(warning, PARSE_ERR, msg);
|
|
}
|
|
}
|
|
|
|
fn read_message_internal(fd: RawFd, vars: &mut VarTable) -> UvarFormat {
|
|
// Read everything from the fd. Put a sane limit on it.
|
|
let mut contents = vec![];
|
|
let mut buffer = [0_u8; 4096];
|
|
while contents.len() < MAX_READ_SIZE {
|
|
match read_loop(&fd, &mut buffer) {
|
|
Ok(0) | Err(_) => break,
|
|
Ok(amt) => contents.extend_from_slice(&buffer[..amt]),
|
|
}
|
|
}
|
|
|
|
// Handle overlong files.
|
|
if contents.len() > MAX_READ_SIZE {
|
|
contents.truncate(MAX_READ_SIZE);
|
|
// Back up to a newline.
|
|
let newline = contents.iter().rposition(|c| *c == b'\n').unwrap_or(0);
|
|
contents.truncate(newline);
|
|
}
|
|
|
|
Self::populate_variables(&contents, vars)
|
|
}
|
|
|
|
// Write our file contents.
|
|
// Return true on success, false on failure.
|
|
fn save(&mut self, directory: &wstr) -> bool {
|
|
use crate::common::ScopeGuard;
|
|
assert!(self.ok_to_save, "It's not OK to save");
|
|
|
|
// Open adjacent temporary file.
|
|
let mut private_file_path = WString::new();
|
|
let Ok(private_fd) = self.open_temporary_file(directory, &mut private_file_path) else {
|
|
return false;
|
|
};
|
|
// unlink pfp upon failure. In case of success, it (already) won't exist.
|
|
let delete_pfp = ScopeGuard::new(private_file_path, |path| {
|
|
wunlink(path);
|
|
});
|
|
let private_file_path = &delete_pfp;
|
|
|
|
// Write to it.
|
|
if self.write_to_fd(&private_fd, private_file_path).is_err() {
|
|
FLOG!(uvar_file, "universal log write_to_fd() failed");
|
|
return false;
|
|
}
|
|
|
|
let real_path = wrealpath(&self.vars_path).unwrap_or_else(|| self.vars_path.clone());
|
|
|
|
// Ensure we maintain ownership and permissions (#2176).
|
|
if let Ok(md) = wstat(&real_path) {
|
|
if unsafe { libc::fchown(private_fd.as_raw_fd(), md.uid(), md.gid()) } == -1 {
|
|
FLOG!(uvar_file, "universal log fchown() failed");
|
|
}
|
|
#[allow(clippy::useless_conversion)]
|
|
let mode: libc::mode_t = md.mode().try_into().unwrap();
|
|
if unsafe { libc::fchmod(private_fd.as_raw_fd(), mode) } == -1 {
|
|
FLOG!(uvar_file, "universal log fchmod() failed");
|
|
}
|
|
}
|
|
|
|
// Linux by default stores the mtime with low precision, low enough that updates that occur
|
|
// in quick succession may result in the same mtime (even the nanoseconds field). So
|
|
// manually set the mtime of the new file to a high-precision clock. Note that this is only
|
|
// necessary because Linux aggressively reuses inodes, causing the ABA problem; on other
|
|
// platforms we tend to notice the file has changed due to a different inode (or file size!)
|
|
//
|
|
// The current time within the Linux kernel is cached, and generally only updated on a timer
|
|
// interrupt. So if the timer interrupt is running at 10 milliseconds, the cached time will
|
|
// only be updated once every 10 milliseconds.
|
|
//
|
|
// It's probably worth finding a simpler solution to this. The tests ran into this, but it's
|
|
// unlikely to affect users.
|
|
#[cfg(any(target_os = "linux", target_os = "android"))]
|
|
{
|
|
let mut times: [libc::timespec; 2] = unsafe { std::mem::zeroed() };
|
|
times[0].tv_nsec = libc::UTIME_OMIT; // don't change ctime
|
|
if unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, &mut times[1]) } != 0 {
|
|
unsafe {
|
|
libc::futimens(private_fd.as_raw_fd(), ×[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply new file.
|
|
if !self.move_new_vars_file_into_place(private_file_path, &real_path) {
|
|
FLOG!(
|
|
uvar_file,
|
|
"universal log move_new_vars_file_into_place() failed"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// Success at last. All of our modified variables have now been written out.
|
|
self.modified.clear();
|
|
ScopeGuard::cancel(delete_pfp);
|
|
true
|
|
}
|
|
}
|
|
|
|
/// Return the default variable path, or an empty string on failure.
|
|
pub fn default_vars_path() -> WString {
|
|
if let Some(mut path) = default_vars_path_directory() {
|
|
path.push_str("/fish_variables");
|
|
return path;
|
|
}
|
|
WString::new()
|
|
}
|
|
|
|
/// Error message.
|
|
const PARSE_ERR: &wstr = L!("Unable to parse universal variable message: '%ls'");
|
|
|
|
/// Small note about not editing ~/.fishd manually. Inserted at the top of all .fishd files.
|
|
const SAVE_MSG: &[u8] = b"# This file contains fish universal variable definitions.\n";
|
|
|
|
/// Version for fish 3.0
|
|
const UVARS_VERSION_3_0: &[u8] = b"3.0";
|
|
|
|
// Maximum file size we'll read.
|
|
const MAX_READ_SIZE: usize = 16 * 1024 * 1024;
|
|
|
|
// Fields used in fish 2.x uvars.
|
|
|
|
mod fish2x_uvars {
|
|
pub const SET: &[u8] = b"SET";
|
|
pub const SET_EXPORT: &[u8] = b"SET_EXPORT";
|
|
}
|
|
// Fields used in fish 3.0 uvars
|
|
mod fish3_uvars {
|
|
pub const SETUVAR: &[u8] = b"SETUVAR";
|
|
pub const EXPORT: &[u8] = b"--export";
|
|
pub const PATH: &[u8] = b"--path";
|
|
}
|
|
|
|
/// Return the default variable path, or an empty string on failure.
|
|
fn default_vars_path_directory() -> Option<WString> {
|
|
path_get_config()
|
|
}
|
|
|
|
/// Test if the message msg contains the command cmd.
|
|
/// On success, updates the cursor to just past the command.
|
|
fn r#match(inout_cursor: &mut &wstr, cmd: &[u8]) -> bool {
|
|
let cursor = *inout_cursor;
|
|
if !cmd
|
|
.iter()
|
|
.copied()
|
|
.map(char::from)
|
|
.eq(cursor.chars().take(cmd.len()))
|
|
{
|
|
return false;
|
|
}
|
|
let len = cmd.len();
|
|
if cursor.len() != len && !matches!(cursor.char_at(len), ' ' | '\t') {
|
|
return false;
|
|
}
|
|
*inout_cursor = &cursor[len..];
|
|
true
|
|
}
|
|
|
|
/// The universal variable format has some funny escaping requirements; here we try to be safe.
|
|
fn is_universal_safe_to_encode_directly(c: char) -> bool {
|
|
if !(32..=128).contains(&u32::from(c)) {
|
|
return false;
|
|
}
|
|
|
|
c.is_alphanumeric() || matches!(c, '/' | '_')
|
|
}
|
|
|
|
/// Escape specified string.
|
|
fn full_escape(input: &wstr) -> WString {
|
|
let mut out = WString::new();
|
|
for c in input.chars() {
|
|
if is_universal_safe_to_encode_directly(c) {
|
|
out.push(c);
|
|
} else if c.is_ascii() {
|
|
sprintf!(=> &mut out, "\\x%.2x", u32::from(c));
|
|
} else if let Some(encoded_byte) = decode_byte_from_char(c) {
|
|
sprintf!(=> &mut out, "\\x%.2x", u32::from(encoded_byte));
|
|
} else if u32::from(c) < 65536 {
|
|
sprintf!(=> &mut out, "\\u%.4x", u32::from(c));
|
|
} else {
|
|
sprintf!(=> &mut out, "\\U%.8x", u32::from(c));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Converts input to UTF-8 and appends it to receiver.
|
|
fn append_utf8(input: &wstr, receiver: &mut Vec<u8>) {
|
|
// Notably we convert between wide and narrow strings without decoding our private-use
|
|
// characters.
|
|
receiver.extend_from_slice(input.to_string().as_bytes());
|
|
}
|
|
|
|
/// Creates a file entry like "SET fish_color_cwd:FF0". Appends the result to *result (as UTF8).
|
|
/// Returns true on success. storage may be used for temporary storage, to avoid allocations.
|
|
fn append_file_entry(
|
|
flags: EnvVarFlags,
|
|
key_in: &wstr,
|
|
val_in: &wstr,
|
|
result: &mut Vec<u8>,
|
|
) -> bool {
|
|
use fish3_uvars as f3;
|
|
|
|
// Record the length on entry, in case we need to back up.
|
|
let mut success = true;
|
|
let result_length_on_entry = result.len();
|
|
|
|
// Append SETVAR header.
|
|
result.extend_from_slice(f3::SETUVAR);
|
|
result.push(b' ');
|
|
|
|
// Append flags.
|
|
if flags.contains(EnvVarFlags::EXPORT) {
|
|
result.extend_from_slice(f3::EXPORT);
|
|
result.push(b' ');
|
|
}
|
|
if flags.contains(EnvVarFlags::PATHVAR) {
|
|
result.extend_from_slice(f3::PATH);
|
|
result.push(b' ');
|
|
}
|
|
|
|
// Append variable name like "fish_color_cwd".
|
|
if !valid_var_name(key_in) {
|
|
FLOGF!(error, "Illegal variable name: '%ls'", key_in);
|
|
success = false;
|
|
}
|
|
if success {
|
|
append_utf8(key_in, result);
|
|
}
|
|
|
|
// Append ":".
|
|
if success {
|
|
result.push(b':');
|
|
}
|
|
|
|
// Append value.
|
|
if success {
|
|
append_utf8(&full_escape(val_in), result);
|
|
}
|
|
|
|
// Append newline.
|
|
if success {
|
|
result.push(b'\n');
|
|
}
|
|
|
|
// Don't modify result on failure. It's sufficient to simply resize it since all we ever did was
|
|
// append to it.
|
|
if !success {
|
|
result.truncate(result_length_on_entry);
|
|
}
|
|
|
|
success
|
|
}
|
|
|
|
/// Encoding of a null string.
|
|
const ENV_NULL: &wstr = L!("\x1d");
|
|
|
|
/// Character used to separate arrays in universal variables file.
|
|
/// This is 30, the ASCII record separator.
|
|
const UVAR_ARRAY_SEP: char = '\x1e';
|
|
|
|
/// Decode a serialized universal variable value into a list.
|
|
fn decode_serialized(val: &wstr) -> Vec<WString> {
|
|
if val == ENV_NULL {
|
|
return vec![];
|
|
}
|
|
val.split(UVAR_ARRAY_SEP).map(|v| v.to_owned()).collect()
|
|
}
|
|
|
|
/// Decode a a list into a serialized universal variable value.
|
|
fn encode_serialized(vals: &[WString]) -> WString {
|
|
if vals.is_empty() {
|
|
return ENV_NULL.to_owned();
|
|
}
|
|
join_strings(vals, UVAR_ARRAY_SEP)
|
|
}
|
|
|
|
/// Try locking the file.
|
|
/// Return true on success, false on error.
|
|
fn flock_uvar_file(file: &mut File) -> bool {
|
|
let start_time = timef();
|
|
while unsafe { libc::flock(file.as_raw_fd(), LOCK_EX) } == -1 {
|
|
if errno().0 != EINTR {
|
|
return false; // do nothing per issue #2149
|
|
}
|
|
}
|
|
let duration = timef() - start_time;
|
|
if duration > 0.25 {
|
|
FLOG!(
|
|
warning,
|
|
wgettext_fmt!(
|
|
"Locking the universal var file took too long (%.3f seconds).",
|
|
duration
|
|
)
|
|
);
|
|
return false;
|
|
}
|
|
true
|
|
}
|
|
|
|
fn skip_spaces(mut s: &wstr) -> &wstr {
|
|
while s.starts_with(L!(" ")) || s.starts_with(L!("\t")) {
|
|
s = &s[1..];
|
|
}
|
|
s
|
|
}
|