fish-shell/src/env_universal_common.rs

1038 lines
36 KiB
Rust
Raw Normal View History

#![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::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};
2024-01-21 07:21:49 +08:00
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::mem::MaybeUninit;
use std::os::fd::{AsFd, AsRawFd, OwnedFd, 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"))]
2024-01-21 07:21:49 +08:00
const O_EXLOCK: OFlag = OFlag::O_EXLOCK;
#[cfg(not(any(bsd, target_os = "macos")))]
2024-01-21 07:21:49 +08:00
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 vars_fd = self.open_and_acquire_lock();
2024-01-21 06:13:13 +08:00
let Some(vars_fd) = vars_fd else {
FLOG!(uvar_file, "universal log open_and_acquire_lock() failed");
return false;
2024-01-21 06:13:13 +08:00
};
// Read from it.
2024-01-21 06:13:13 +08:00
self.load_from_fd(vars_fd.as_raw_fd(), 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;
}
2024-01-21 08:31:32 +08:00
let Ok(fd) = open_cloexec(&self.narrow_vars_path, OFlag::O_RDONLY, Mode::empty()) else {
2024-01-21 06:13:13 +08:00
return false;
};
FLOG!(uvar_file, "universal log reading from file");
self.load_from_fd(fd.as_raw_fd(), callbacks);
true
}
fn load_from_fd(&mut self, fd: RawFd, callbacks: &mut CallbackDataList) {
// Get the dev / inode.
let current_file = file_id_for_fd(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(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.
2024-01-21 06:13:13 +08:00
fn open_and_acquire_lock(&mut self) -> Option<OwnedFd> {
// 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;
2024-01-21 07:21:49 +08:00
let mut flags = OFlag::O_RDWR | OFlag::O_CREAT;
2024-01-21 07:21:49 +08:00
if !O_EXLOCK.is_empty() && self.do_flock {
flags |= O_EXLOCK;
locked_by_open = true;
}
2024-01-21 06:13:13 +08:00
let mut res_fd = None;
while res_fd.is_none() {
let fd = match wopen_cloexec(&self.vars_path, flags, Mode::from_bits_truncate(0o644)) {
2024-01-21 08:31:32 +08:00
Ok(fd) => fd,
2024-01-21 06:13:13 +08:00
Err(err) => {
if err == nix::Error::EINTR {
continue; // signaled; try again
}
2024-01-21 07:21:49 +08:00
if !O_EXLOCK.is_empty() {
if flags.intersects(O_EXLOCK)
2024-01-21 06:13:13 +08:00
&& [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;
}
}
2024-01-21 06:13:13 +08:00
FLOG!(
error,
wgettext_fmt!(
"Unable to open universal variable file '%s': %s",
&self.vars_path,
err.to_string()
)
);
break;
}
2024-01-21 06:13:13 +08:00
};
// 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 {
2024-01-21 06:13:13 +08:00
if !flock_uvar_file(fd.as_raw_fd()) {
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.
2024-01-21 06:13:13 +08:00
if file_id_for_fd(fd.as_raw_fd()) != file_id_for_path(&self.vars_path) {
// Oops, it changed! Try again.
2024-01-21 06:13:13 +08:00
drop(fd);
} else {
res_fd = Some(fd);
}
}
2024-01-21 06:13:13 +08:00
res_fd
}
fn open_temporary_file(
&mut self,
directory: &wstr,
out_path: &mut WString,
) -> Result<OwnedFd, 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.as_raw_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(), &times[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 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(fd: RawFd) -> bool {
let start_time = timef();
while unsafe { libc::flock(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
}