mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-29 06:13:20 +00:00
e01fc62d69
When we read bytes like \xfc that don't produce a Unicode code point, we encode them in a Unicode private use area. This encoding should be transparent to the user. We accidentally add it to uvar files as \uf6fc in this case. When reading it back, read_unquoted_escape() will fail at the "fish_reserved_codepoint(c)" check. This check is to avoid external input being misinterpreted as one of our in-band signalling characters like ANY_CHAR (for *). For encoded raw bytes, this check probably doesn't really matter in terms of security because the only thing we do with these bytes is convert them back to raw. So we could allow unescaping them at this point, thus supporting old uvar files. However that seems like the wrong direction. PUA encoding should never leak. So let's instead make sure to serialize it as \xfc instead of \f6fc going forward. Fixes #10313
1037 lines
36 KiB
Rust
1037 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, split_string, 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>,
|
|
}
|
|
impl CallbackData {
|
|
/// Construct from a key and maybe a value.
|
|
pub fn new(key: WString, val: Option<EnvVar>) -> Self {
|
|
Self { key, val }
|
|
}
|
|
/// \return whether this callback represents an erased variable.
|
|
pub fn is_erase(&self) -> bool {
|
|
self.val.is_none()
|
|
}
|
|
}
|
|
|
|
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 \p out_vars from a \p 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 \p 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.
|
|
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::new(key.clone(), 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::new(key.clone(), 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).
|
|
// let mut sbuf : libc::stat = MaybeUninit::uninit();
|
|
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![];
|
|
}
|
|
split_string(val, UVAR_ARRAY_SEP)
|
|
}
|
|
|
|
/// 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
|
|
}
|