use std::ffi::{CString, OsString};
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::os::unix::ffi::OsStrExt as _;
use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use clap::Parser as _;
use clap_complete::env::{Bash, Elvish, EnvCompleter, Fish, Powershell, Shells, Zsh};
use super::complete::public_completion_command;
const MARKER: &str = "# hf2q-managed dynamic completion — auto-provisioned, edits are overwritten";
pub(super) const MARKER_PREFIX: &str = "# hf2q-managed dynamic completion";
pub(super) const BINDING_PREFIX: &str = "# hf2q-completion-binding sha256:";
const BINDING_PLACEHOLDER: &str =
"# hf2q-completion-binding sha256:0000000000000000000000000000000000000000000000000000000000000000";
const OPT_OUT_VAR: &str = "HF2Q_NO_COMPLETION_INSTALL";
const TRIGGER_VAR: &str = "HF2Q_COMPLETE";
#[derive(Copy, Clone, Debug)]
struct ProtectedBash;
#[derive(Copy, Clone, Debug)]
struct ProtectedZsh;
#[derive(Copy, Clone, Debug)]
struct ProtectedFish;
const PROTECTED_BASH: ProtectedBash = ProtectedBash;
const PROTECTED_ZSH: ProtectedZsh = ProtectedZsh;
const PROTECTED_FISH: ProtectedFish = ProtectedFish;
#[derive(Copy, Clone, Debug)]
enum CandidateSeparator {
EnvironmentOrNewline,
Newline,
}
#[derive(Copy, Clone, Debug)]
struct PublicOnly<S> {
inner: S,
separator: CandidateSeparator,
}
impl<S> EnvCompleter for PublicOnly<S>
where
S: EnvCompleter,
{
fn name(&self) -> &'static str {
self.inner.name()
}
fn is(&self, name: &str) -> bool {
self.inner.is(name)
}
fn write_registration(
&self,
var: &str,
name: &str,
bin: &str,
completer: &str,
buf: &mut dyn Write,
) -> std::io::Result<()> {
self.inner
.write_registration(var, name, bin, completer, buf)
}
fn write_complete(
&self,
cmd: &mut clap::Command,
args: Vec<OsString>,
current_dir: Option<&Path>,
buf: &mut dyn Write,
) -> std::io::Result<()> {
let separator = match self.separator {
CandidateSeparator::EnvironmentOrNewline => std::env::var("_CLAP_IFS")
.ok()
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "\n".to_owned()),
CandidateSeparator::Newline => "\n".to_owned(),
};
let mut rendered = Vec::new();
self.inner
.write_complete(cmd, args, current_dir, &mut rendered)?;
let rendered = std::str::from_utf8(&rendered).map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("completion protocol was not UTF-8: {error}"),
)
})?;
let filtered = rendered
.split(&separator)
.filter(|record| record.is_empty() || public_record(record))
.collect::<Vec<_>>()
.join(&separator);
buf.write_all(filtered.as_bytes())
}
}
fn public_record(record: &str) -> bool {
const HIDDEN: &[&str] = &[
"__standalone-install",
"__fetch-hub-gguf",
"__catalog-hub-gguf",
"__verify-local-gguf",
"source-teacher",
"source-teacher-reference",
"source-teacher-acceptance-verify",
"--chat-parent-lifeline-fd",
];
!HIDDEN.iter().any(|hidden| {
record == *hidden
|| record
.strip_prefix(hidden)
.is_some_and(|suffix| suffix.starts_with('\t') || suffix.starts_with(':'))
})
}
const PUBLIC_BASH: PublicOnly<ProtectedBash> = PublicOnly {
inner: PROTECTED_BASH,
separator: CandidateSeparator::EnvironmentOrNewline,
};
const PUBLIC_ELVISH: PublicOnly<Elvish> = PublicOnly {
inner: Elvish,
separator: CandidateSeparator::EnvironmentOrNewline,
};
const PUBLIC_FISH: PublicOnly<ProtectedFish> = PublicOnly {
inner: PROTECTED_FISH,
separator: CandidateSeparator::Newline,
};
const PUBLIC_POWERSHELL: PublicOnly<Powershell> = PublicOnly {
inner: Powershell,
separator: CandidateSeparator::Newline,
};
const PUBLIC_ZSH: PublicOnly<ProtectedZsh> = PublicOnly {
inner: PROTECTED_ZSH,
separator: CandidateSeparator::EnvironmentOrNewline,
};
fn completion_shells() -> Shells<'static> {
Shells(&[
&PUBLIC_BASH,
&PUBLIC_ELVISH,
&PUBLIC_FISH,
&PUBLIC_POWERSHELL,
&PUBLIC_ZSH,
])
}
pub fn complete_env() {
clap_complete::CompleteEnv::with_factory(public_completion_command)
.var(TRIGGER_VAR)
.shells(completion_shells())
.complete();
}
impl EnvCompleter for ProtectedBash {
fn name(&self) -> &'static str {
Bash.name()
}
fn is(&self, name: &str) -> bool {
Bash.is(name)
}
fn write_registration(
&self,
var: &str,
name: &str,
bin: &str,
completer: &str,
buf: &mut dyn Write,
) -> std::io::Result<()> {
const COMPLETER_PLACEHOLDER: &str = "__HF2Q_COMPLETER_PATH_PLACEHOLDER__";
let mut upstream = Vec::new();
Bash.write_registration(var, name, bin, COMPLETER_PLACEHOLDER, &mut upstream)?;
buf.write_all(&protect_bash_registration(
&upstream,
COMPLETER_PLACEHOLDER,
completer,
)?)
}
fn write_complete(
&self,
cmd: &mut clap::Command,
args: Vec<OsString>,
current_dir: Option<&Path>,
buf: &mut dyn Write,
) -> std::io::Result<()> {
Bash.write_complete(cmd, args, current_dir, buf)
}
}
impl EnvCompleter for ProtectedZsh {
fn name(&self) -> &'static str {
Zsh.name()
}
fn is(&self, name: &str) -> bool {
Zsh.is(name)
}
fn write_registration(
&self,
var: &str,
name: &str,
bin: &str,
completer: &str,
buf: &mut dyn Write,
) -> std::io::Result<()> {
const COMPLETER_PLACEHOLDER: &str = "__HF2Q_COMPLETER_PATH_PLACEHOLDER__";
let mut upstream = Vec::new();
Zsh.write_registration(var, name, bin, COMPLETER_PLACEHOLDER, &mut upstream)?;
buf.write_all(&protect_zsh_registration(
&upstream,
COMPLETER_PLACEHOLDER,
completer,
)?)
}
fn write_complete(
&self,
cmd: &mut clap::Command,
args: Vec<OsString>,
current_dir: Option<&Path>,
buf: &mut dyn Write,
) -> std::io::Result<()> {
Zsh.write_complete(cmd, args, current_dir, buf)
}
}
impl EnvCompleter for ProtectedFish {
fn name(&self) -> &'static str {
Fish.name()
}
fn is(&self, name: &str) -> bool {
Fish.is(name)
}
fn write_registration(
&self,
var: &str,
name: &str,
bin: &str,
completer: &str,
buf: &mut dyn Write,
) -> std::io::Result<()> {
const COMPLETER_PLACEHOLDER: &str = "__HF2Q_COMPLETER_PATH_PLACEHOLDER__";
let mut upstream = Vec::new();
Fish.write_registration(var, name, bin, COMPLETER_PLACEHOLDER, &mut upstream)?;
buf.write_all(&protect_fish_registration(
&upstream,
COMPLETER_PLACEHOLDER,
completer,
)?)
}
fn write_complete(
&self,
cmd: &mut clap::Command,
args: Vec<OsString>,
current_dir: Option<&Path>,
buf: &mut dyn Write,
) -> std::io::Result<()> {
Fish.write_complete(cmd, args, current_dir, buf)
}
}
fn protect_fish_registration(
upstream: &[u8],
completer_placeholder: &str,
completer: &str,
) -> std::io::Result<Vec<u8>> {
let upstream = std::str::from_utf8(upstream).map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Fish registration UTF-8: {error}"),
)
})?;
const ARGUMENTS_OPEN: &str = "--arguments \"(";
const ARGUMENTS_CLOSE: &str = ")\"\n";
if upstream.matches(ARGUMENTS_OPEN).count() != 1
|| !upstream.ends_with(ARGUMENTS_CLOSE)
|| upstream.matches(completer_placeholder).count() != 1
{
return Err(std::io::Error::other(
"pinned clap_complete Fish registration shape changed",
));
}
let open = upstream.find(ARGUMENTS_OPEN).expect("counted above") + ARGUMENTS_OPEN.len();
let inner = &upstream[open..upstream.len() - ARGUMENTS_CLOSE.len()];
if !inner.contains(completer_placeholder) {
return Err(std::io::Error::other(
"pinned clap_complete Fish registration must invoke the completer in --arguments",
));
}
let shell_quoted_completer =
format!("'{}'", completer.replace('\\', "\\\\").replace('\'', "\\'"));
let invocation = inner.replace(completer_placeholder, "$_hf2q_completer");
let registration = &upstream[..open];
let protected = format!(
"function __hf2q_dynamic_completer\n \
set -l _hf2q_completer {shell_quoted_completer}\n \
if not test -f \"$_hf2q_completer\" -a -x \"$_hf2q_completer\"\n \
set _hf2q_completer (command -v hf2q)\n \
or return\n \
end\n \
{invocation}\n\
end\n\n\
{registration}__hf2q_dynamic_completer{ARGUMENTS_CLOSE}"
);
Ok(protected.into_bytes())
}
fn protect_bash_registration(
upstream: &[u8],
completer_placeholder: &str,
completer: &str,
) -> std::io::Result<Vec<u8>> {
let upstream = std::str::from_utf8(upstream).map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Bash registration UTF-8: {error}"),
)
})?;
const ASSIGNMENT: &str = " COMPREPLY=( $( \\\n";
const STATUS: &str = " if [[ $? != 0 ]]; then\n";
let quoted_placeholder = format!("\"{completer_placeholder}\"");
if upstream.matches(ASSIGNMENT).count() != 1
|| upstream.matches(STATUS).count() != 1
|| upstream.matches("ed_placeholder).count() != 1
{
return Err(std::io::Error::other(
"pinned clap_complete Bash registration shape changed",
));
}
let shell_quoted_completer = format!("'{}'", completer.replace('\'', "'\"'\"'"));
let protected = upstream.replace("ed_placeholder, "\"$_hf2q_completer\"");
let protected = protected.replace(
STATUS,
" local _hf2q_complete_status=$?\n\
if [[ -n $_hf2q_restore_glob ]]; then\n\
set +f\n\
fi\n\
if [[ $_hf2q_complete_status != 0 ]]; then\n",
);
let protected = protected.replace(
ASSIGNMENT,
&format!(
" local _hf2q_completer={shell_quoted_completer}\n\
if [[ ! -f $_hf2q_completer || ! -x $_hf2q_completer ]]; then\n\
_hf2q_completer=$(type -P hf2q) && [[ -f $_hf2q_completer && -x $_hf2q_completer ]] || {{ unset COMPREPLY; return 0; }}\n\
fi\n\
local _hf2q_restore_glob=\n\
if [[ $- != *f* ]]; then\n\
set -f\n\
_hf2q_restore_glob=1\n\
fi\n\
COMPREPLY=( $( \\\n"
),
);
Ok(protected.into_bytes())
}
fn protect_zsh_registration(
upstream: &[u8],
completer_placeholder: &str,
completer: &str,
) -> std::io::Result<Vec<u8>> {
let upstream = std::str::from_utf8(upstream).map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Zsh registration UTF-8: {error}"),
)
})?;
const LOCALIZE_ARRAYS: &str = " setopt localoptions noksharrays\n";
if upstream.matches(completer_placeholder).count() != 1 {
return Err(std::io::Error::other(
"pinned clap_complete Zsh registration must invoke the completer exactly once",
));
}
let upstream = upstream.replace(completer_placeholder, "\"$_hf2q_completer\"");
let Some(start) = upstream.find("function _clap_dynamic_completer_") else {
return Err(std::io::Error::other(
"pinned clap_complete Zsh registration shape changed",
));
};
let Some(relative_open) = upstream[start..].find("() {\n") else {
return Err(std::io::Error::other(
"pinned clap_complete Zsh function opening changed",
));
};
let insertion = start + relative_open + "() {\n".len();
if upstream[insertion..].contains(" emulate -L zsh\n")
|| upstream[insertion..].contains(LOCALIZE_ARRAYS)
{
return Err(std::io::Error::other(
"pinned clap_complete Zsh registration unexpectedly localizes options",
));
}
let shell_quoted_completer = format!("'{}'", completer.replace('\'', "'\"'\"'"));
let fallback = format!(
" local _hf2q_completer={shell_quoted_completer}\n \
if [[ ! -f $_hf2q_completer || ! -x $_hf2q_completer ]]; then\n \
_hf2q_completer=$(whence -p hf2q) || return 0\n \
[[ -f $_hf2q_completer && -x $_hf2q_completer ]] || return 0\n \
fi\n"
);
let mut protected =
String::with_capacity(upstream.len() + LOCALIZE_ARRAYS.len() + fallback.len());
protected.push_str(&upstream[..insertion]);
protected.push_str(LOCALIZE_ARRAYS);
protected.push_str(&fallback);
protected.push_str(&upstream[insertion..]);
Ok(protected.into_bytes())
}
const ZSH_DIR_VAR: &str = "HF2Q_ZSH_COMPLETIONS_DIR";
const FISH_DIR_VAR: &str = "HF2Q_FISH_COMPLETIONS_DIR";
struct Shell {
name: &'static str,
file: &'static str,
marker_line: usize,
explicit_dir_var: &'static str,
}
const BASH: Shell = Shell {
name: "bash",
file: "hf2q",
marker_line: 0,
explicit_dir_var: "BASH_COMPLETION_USER_DIR",
};
const ZSH: Shell = Shell {
name: "zsh",
file: "_hf2q",
marker_line: 1,
explicit_dir_var: ZSH_DIR_VAR,
};
const FISH: Shell = Shell {
name: "fish",
file: "hf2q.fish",
marker_line: 0,
explicit_dir_var: FISH_DIR_VAR,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct FileIdentity {
dev: u64,
ino: u64,
size: u64,
mtime: i64,
mtime_nsec: i64,
ctime: i64,
ctime_nsec: i64,
}
impl FileIdentity {
fn from_metadata(metadata: &fs::Metadata) -> Self {
Self {
dev: metadata.dev(),
ino: metadata.ino(),
size: metadata.size(),
mtime: metadata.mtime(),
mtime_nsec: metadata.mtime_nsec(),
ctime: metadata.ctime(),
ctime_nsec: metadata.ctime_nsec(),
}
}
}
#[derive(Clone, Debug)]
pub(super) enum ExpectedTarget {
Absent,
Regular {
identity: FileIdentity,
bytes: Vec<u8>,
mode: u32,
},
}
pub(super) fn capture_regular_target(path: &Path) -> std::io::Result<ExpectedTarget> {
let before = fs::symlink_metadata(path)?;
if !before.file_type().is_file() {
return Err(std::io::Error::other("target is not a regular file"));
}
let identity = FileIdentity::from_metadata(&before);
let bytes = fs::read(path)?;
let after = fs::symlink_metadata(path)?;
if !after.file_type().is_file() || FileIdentity::from_metadata(&after) != identity {
return Err(std::io::Error::other(
"target changed while it was being inspected",
));
}
Ok(ExpectedTarget::Regular {
identity,
bytes,
mode: before.permissions().mode() & 0o7777,
})
}
impl ExpectedTarget {
pub(super) fn bytes(&self) -> &[u8] {
match self {
Self::Absent => &[],
Self::Regular { bytes, .. } => bytes,
}
}
pub(super) fn mode_or(&self, default: u32) -> u32 {
match self {
Self::Absent => default,
Self::Regular { mode, .. } => *mode,
}
}
pub(super) fn revalidate(&self, target: &Path) -> std::io::Result<()> {
match self {
Self::Absent => match fs::symlink_metadata(target) {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Ok(_) => Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"target appeared during reconciliation",
)),
Err(error) => Err(error),
},
Self::Regular {
identity, bytes, ..
} => {
let before = fs::symlink_metadata(target)?;
if !before.file_type().is_file()
|| FileIdentity::from_metadata(&before) != *identity
{
return Err(std::io::Error::other(
"target identity changed during reconciliation",
));
}
if fs::read(target)? != *bytes {
return Err(std::io::Error::other(
"target content changed during reconciliation",
));
}
let after = fs::symlink_metadata(target)?;
if !after.file_type().is_file() || FileIdentity::from_metadata(&after) != *identity
{
return Err(std::io::Error::other(
"target changed during final reconciliation check",
));
}
Ok(())
}
}
}
}
fn non_empty(v: std::ffi::OsString) -> Option<std::ffi::OsString> {
if v.is_empty() {
None
} else {
Some(v)
}
}
#[derive(Debug)]
enum Outcome {
Wrote(PathBuf),
UpToDate(PathBuf),
Adopted {
path: PathBuf,
backup: Option<PathBuf>,
},
PreservedForeign { path: PathBuf, backup_error: String },
PreservedOperatorLink(PathBuf),
PreservedDanglingLink(PathBuf),
PreservedNonRegular(PathBuf),
OptedOut,
CompletionRun,
LifecycleCleanup,
PolicySkip(&'static str),
Failed(String),
Startup(super::completion_startup::Outcome),
}
static LAST_OUTCOME: OnceLock<Vec<(&'static str, Outcome)>> = OnceLock::new();
pub fn reconcile(raw_args: &[OsString]) {
let outcomes: Vec<(&'static str, Outcome)> = if completion_trigger_active() {
vec![("completion", Outcome::CompletionRun)]
} else if lifecycle_cleanup_requested(raw_args) {
vec![("completion", Outcome::LifecycleCleanup)]
} else if std::env::var_os(OPT_OUT_VAR).is_some() {
vec![("completion", Outcome::OptedOut)]
} else {
let allow_automatic = automatic_destinations_enabled();
let mut v = Vec::new();
for shell in [&BASH, &ZSH, &FISH] {
let dirs = completions_dirs(shell, allow_automatic);
if dirs.is_empty() {
let outcome = if !allow_automatic {
Outcome::PolicySkip(shell.explicit_dir_var)
} else {
Outcome::Failed(format!(
"cannot resolve a {} completions dir (HOME/XDG base unset)",
shell.name
))
};
v.push((shell.name, outcome));
continue;
}
for dir in dirs {
let o = try_reconcile_in(shell, &dir).unwrap_or_else(Outcome::Failed);
v.push((shell.name, o));
}
}
let bash_registration = startup_bash_registration();
let zsh_functions_dir = preferred_zsh_startup_registration();
for (shell, outcome) in super::completion_startup::reconcile_preferred_shell(
bash_registration
.as_ref()
.map(|(path, binding)| (path.as_path(), binding.as_str())),
zsh_functions_dir
.as_ref()
.map(|(path, binding)| (path.as_path(), binding.as_str())),
allow_automatic,
) {
v.push((shell, Outcome::Startup(outcome)));
}
let registrations = v
.iter()
.filter_map(|(_, outcome)| match outcome {
Outcome::Wrote(path) | Outcome::UpToDate(path) => Some(path.clone()),
Outcome::Adopted { path, .. } => Some(path.clone()),
_ => None,
})
.collect::<Vec<_>>();
let startup_files = v
.iter()
.filter_map(|(_, outcome)| match outcome {
Outcome::Startup(
super::completion_startup::Outcome::Wrote(path)
| super::completion_startup::Outcome::UpToDate(path),
) => Some(path.clone()),
_ => None,
})
.collect::<Vec<_>>();
if let Err(error) = super::completion_receipt::record(®istrations, &startup_files) {
v.push(("ownership receipt", Outcome::Failed(error)));
}
v
};
let _ = LAST_OUTCOME.set(outcomes);
}
pub fn report_outcome() {
let Some(outcomes) = LAST_OUTCOME.get() else {
return;
};
let mut updated = Vec::new();
let mut backups = Vec::new();
let mut problems = Vec::new();
for (shell, outcome) in outcomes {
let family = shell.split_whitespace().next().unwrap_or(shell);
if matches!(outcome, Outcome::Wrote(_) | Outcome::Adopted { .. })
|| matches!(
outcome,
Outcome::Startup(super::completion_startup::Outcome::Wrote(_))
)
{
if !updated.contains(&family) {
updated.push(family);
}
}
match outcome {
Outcome::Wrote(path) => {
tracing::debug!(shell, path = %path.display(), "provisioned completion");
}
Outcome::UpToDate(path) => {
tracing::debug!(shell, path = %path.display(), "completion already current");
}
Outcome::Adopted { path, backup } => {
tracing::debug!(shell, path = %path.display(), ?backup, "adopted completion destination");
if let Some(path) = backup {
backups.push(path.display().to_string());
}
}
Outcome::PreservedForeign { path, backup_error } => problems.push(format!(
"{shell} destination {} was preserved because its backup failed: {backup_error}",
path.display()
)),
Outcome::PreservedOperatorLink(path) => {
tracing::debug!(shell, path = %path.display(), "preserved operator completion symlink");
}
Outcome::PreservedDanglingLink(path) => problems.push(format!(
"{shell} destination {} is a dangling symlink",
path.display()
)),
Outcome::PreservedNonRegular(path) => problems.push(format!(
"{shell} destination {} is not a regular file",
path.display()
)),
Outcome::OptedOut | Outcome::CompletionRun | Outcome::LifecycleCleanup => {}
Outcome::PolicySkip(variable) => {
tracing::debug!(
shell,
explicit_destination = *variable,
"automatic completion provisioning is not authorized for this binary"
);
}
Outcome::Failed(reason) => problems.push(format!("{shell}: {reason}")),
Outcome::Startup(startup) => match startup {
super::completion_startup::Outcome::Wrote(path) => {
tracing::debug!(shell, path = %path.display(), "provisioned completion startup block");
}
super::completion_startup::Outcome::UpToDate(path) => {
tracing::debug!(shell, path = %path.display(), "completion startup block already current");
}
super::completion_startup::Outcome::PreservedMalformed(path) => problems.push(
format!("{shell} markers in {} are ambiguous", path.display()),
),
super::completion_startup::Outcome::PreservedNonRegular(path) => {
problems.push(format!(
"{shell} destination {} is not a regular file",
path.display()
))
}
super::completion_startup::Outcome::Failed(reason) => {
problems.push(format!("{shell}: {reason}"));
}
},
}
}
updated.sort_unstable();
backups.sort();
problems.sort();
if !updated.is_empty() {
eprintln!(
"hf2q: installed Tab completion for {}; open a new shell to activate it",
updated.join(", ")
);
for backup in backups {
eprintln!("hf2q: preserved the previous completion file at {backup}");
}
}
for problem in problems {
eprintln!("hf2q: completion setup incomplete: {problem}");
}
}
fn lifecycle_cleanup_requested(raw_args: &[OsString]) -> bool {
matches!(
super::Cli::try_parse_from(raw_args.iter().cloned()),
Ok(super::Cli {
command: super::Command::Uninstall(_),
..
}) | Ok(super::Cli {
command: super::Command::Update(super::UpdateArgs { rollback: true, .. }),
..
})
)
}
fn automatic_destinations_enabled() -> bool {
if cfg!(debug_assertions) || rustix::process::geteuid().is_root() {
return false;
}
let Ok(executable) = std::env::current_exe() else {
return false;
};
matches!(
crate::distribution::installation::detect(&executable),
Ok(crate::distribution::installation::Installation::Standalone { .. })
| Ok(crate::distribution::installation::Installation::Cargo { .. })
)
}
fn completion_trigger_active() -> bool {
std::env::var_os(TRIGGER_VAR).is_some_and(|value| !value.is_empty() && value != "0")
}
fn try_reconcile_in(shell: &Shell, dir: &Path) -> Result<Outcome, String> {
let target = dir.join(shell.file);
let desired = render_registration(shell).map_err(|e| format!("render failed: {e}"))?;
let mut adoption_backup: Option<Option<PathBuf>> = None;
let expected = match std::fs::symlink_metadata(&target) {
Ok(meta) if meta.file_type().is_symlink() => {
return Ok(match std::fs::metadata(&target) {
Ok(referent) if referent.file_type().is_file() => {
Outcome::PreservedOperatorLink(target)
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
Outcome::PreservedDanglingLink(target)
}
_ => Outcome::PreservedNonRegular(target),
});
}
Ok(meta) if !meta.file_type().is_file() => {
return Ok(Outcome::PreservedNonRegular(target));
}
Ok(_) => {
let expected = capture_regular_target(&target)
.map_err(|e| format!("reading {}: {e}", target.display()))?;
let existing = expected.bytes();
if existing == desired {
return Ok(Outcome::UpToDate(target));
}
if !is_hf2q_managed(existing, shell.marker_line) {
let backup = if is_current_static_output(shell, existing) {
None
} else {
match commit_adoption_backup(dir, shell.file, &expected) {
Ok(slot) => Some(slot),
Err(e) => {
return Ok(Outcome::PreservedForeign {
path: target,
backup_error: e.to_string(),
});
}
}
};
adoption_backup = Some(backup);
}
expected
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => ExpectedTarget::Absent,
Err(e) => return Err(format!("stat {}: {e}", target.display())),
};
std::fs::create_dir_all(dir).map_err(|e| format!("creating {}: {e}", dir.display()))?;
let mode = if adoption_backup.is_some() {
0o644
} else {
expected.mode_or(0o644)
};
atomic_replace_with_hook(dir, &target, &desired, mode, &expected, "completion", || {})
.map_err(|e| format!("writing {}: {e}", target.display()))?;
Ok(match adoption_backup {
Some(backup) => Outcome::Adopted {
path: target,
backup,
},
None => Outcome::Wrote(target),
})
}
fn is_current_static_output(shell: &Shell, existing: &[u8]) -> bool {
let static_shell = match shell.name {
"bash" => clap_complete::Shell::Bash,
"zsh" => clap_complete::Shell::Zsh,
"fish" => clap_complete::Shell::Fish,
_ => return false,
};
{
let mut command = public_completion_command();
let mut generated = Vec::new();
clap_complete::generate(static_shell, &mut command, "hf2q", &mut generated);
generated == existing
}
}
fn adoption_backup_slot(dir: &Path, file_name: &str, bytes: &[u8]) -> PathBuf {
use sha2::{Digest as _, Sha256};
let digest = format!("{:x}", Sha256::digest(bytes));
dir.join(format!(".hf2q-backup.{file_name}.{}", &digest[..12]))
}
fn commit_adoption_backup(
dir: &Path,
file_name: &str,
expected: &ExpectedTarget,
) -> std::io::Result<PathBuf> {
let ExpectedTarget::Regular { bytes, mode, .. } = expected else {
return Err(std::io::Error::other(
"adoption backup requires a captured regular file",
));
};
let slot = adoption_backup_slot(dir, file_name, bytes);
let slot_state = match std::fs::symlink_metadata(&slot) {
Ok(meta) if meta.file_type().is_file() => {
let captured = capture_regular_target(&slot)?;
if captured.bytes() == bytes {
return Ok(slot);
}
captured
}
Ok(_) => {
return Err(std::io::Error::other(format!(
"backup slot {} is occupied by a non-regular file",
slot.display()
)));
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => ExpectedTarget::Absent,
Err(e) => return Err(e),
};
atomic_replace_with_hook(dir, &slot, bytes, *mode, &slot_state, "backup", || {})?;
Ok(slot)
}
fn completions_dirs(shell: &Shell, allow_automatic: bool) -> Vec<PathBuf> {
match shell.name {
"bash" => bash_target_dirs_with_automatic_user(allow_automatic),
"zsh" => zsh_target_dirs_with_automatic_locations(allow_automatic),
"fish" => fish_target_dirs_with_automatic_user(allow_automatic),
_ => Vec::new(),
}
}
fn bash_target_dirs_with_automatic_user(allow_automatic_user: bool) -> Vec<PathBuf> {
let explicit = std::env::var_os("BASH_COMPLETION_USER_DIR")
.and_then(non_empty)
.is_some();
if !explicit && !allow_automatic_user {
return Vec::new();
}
user_completions_dir().into_iter().collect()
}
fn user_completions_dir() -> Option<PathBuf> {
let base = if let Some(d) = std::env::var_os("BASH_COMPLETION_USER_DIR").and_then(non_empty) {
PathBuf::from(d)
} else {
let data_home = if let Some(x) = std::env::var_os("XDG_DATA_HOME").and_then(non_empty) {
PathBuf::from(x)
} else {
let home = std::env::var_os("HOME").and_then(non_empty)?;
PathBuf::from(home).join(".local/share")
};
data_home.join("bash-completion")
};
Some(base.join("completions"))
}
fn zsh_target_dirs_with_automatic_locations(allow_automatic_locations: bool) -> Vec<PathBuf> {
if let Some(d) = std::env::var_os(ZSH_DIR_VAR).and_then(non_empty) {
return vec![PathBuf::from(d)];
}
if !allow_automatic_locations {
return Vec::new();
}
let mut dirs = Vec::new();
if let Some(hb) = safe_on_fpath_zsh_dir(homebrew_site_functions_candidates()) {
dirs.push(hb);
}
if let Some(xdg) = xdg_zsh_site_functions() {
dirs.push(xdg);
}
dirs
}
fn xdg_zsh_site_functions() -> Option<PathBuf> {
let data_home = if let Some(x) = std::env::var_os("XDG_DATA_HOME").and_then(non_empty) {
PathBuf::from(x)
} else {
PathBuf::from(std::env::var_os("HOME").and_then(non_empty)?).join(".local/share")
};
Some(data_home.join("zsh/site-functions"))
}
fn preferred_zsh_startup_registration() -> Option<(PathBuf, String)> {
let desired = render_registration(&ZSH).ok()?;
let binding = registration_binding_line(&desired, &ZSH)?.to_owned();
if let Some(explicit) = std::env::var_os(ZSH_DIR_VAR).and_then(non_empty) {
let dir = PathBuf::from(explicit);
return is_exact_regular_registration(&dir.join(ZSH.file), &desired)
.then_some((dir, binding));
}
let xdg = xdg_zsh_site_functions();
let homebrew = safe_on_fpath_zsh_dir(homebrew_site_functions_candidates());
for dir in [xdg.as_ref(), homebrew.as_ref()].into_iter().flatten() {
if is_exact_regular_registration(&dir.join(ZSH.file), &desired) {
return Some((dir.clone(), binding));
}
}
None
}
fn startup_bash_registration() -> Option<(PathBuf, String)> {
let desired = render_registration(&BASH).ok()?;
let binding = registration_binding_line(&desired, &BASH)?.to_owned();
let path = user_completions_dir()?.join(BASH.file);
is_exact_regular_registration(&path, &desired).then_some((path, binding))
}
fn fish_target_dirs_with_automatic_user(allow_automatic_user: bool) -> Vec<PathBuf> {
if let Some(dir) = std::env::var_os(FISH_DIR_VAR).and_then(non_empty) {
return vec![PathBuf::from(dir)];
}
if !allow_automatic_user {
return Vec::new();
}
fish_user_completions_dir().into_iter().collect()
}
fn fish_user_completions_dir() -> Option<PathBuf> {
let config_home = if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME").and_then(non_empty) {
PathBuf::from(xdg)
} else {
PathBuf::from(std::env::var_os("HOME").and_then(non_empty)?).join(".config")
};
Some(config_home.join("fish/completions"))
}
fn homebrew_site_functions_candidates() -> Vec<PathBuf> {
let mut v = Vec::new();
if let Some(p) = std::env::var_os("HOMEBREW_PREFIX").and_then(non_empty) {
v.push(PathBuf::from(p).join("share/zsh/site-functions"));
}
v.push(PathBuf::from("/opt/homebrew/share/zsh/site-functions"));
v.push(PathBuf::from("/usr/local/share/zsh/site-functions"));
v
}
fn safe_on_fpath_zsh_dir(candidates: Vec<PathBuf>) -> Option<PathBuf> {
use std::os::unix::fs::MetadataExt;
let euid = unsafe { libc::geteuid() };
if euid == 0 {
return None; }
for dir in candidates {
let Ok(meta) = std::fs::symlink_metadata(&dir) else {
continue; };
if !meta.file_type().is_dir() {
continue; }
if meta.uid() != euid {
continue; }
let mode = meta.mode();
if mode & 0o022 != 0 {
continue; }
if mode & 0o300 != 0o300 {
continue; }
return Some(dir);
}
None
}
fn render_registration(shell: &Shell) -> std::io::Result<Vec<u8>> {
let exe = std::env::current_exe()?;
let exe = std::fs::canonicalize(&exe).unwrap_or(exe);
let completer = exe.to_str().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"executable path is not valid UTF-8",
)
})?;
let shells = completion_shells();
let completer_shell = shells.completer(shell.name).ok_or_else(|| {
std::io::Error::other(format!(
"clap_complete has no builtin {} completer",
shell.name
))
})?;
let mut clap = Vec::new();
completer_shell.write_registration("HF2Q_COMPLETE", "hf2q", "hf2q", completer, &mut clap)?;
let mut buf = Vec::new();
if shell.marker_line == 0 {
writeln!(buf, "{MARKER}")?;
writeln!(buf, "{BINDING_PLACEHOLDER}")?;
buf.extend_from_slice(&clap);
} else {
let nl = clap
.iter()
.position(|&b| b == b'\n')
.ok_or_else(|| std::io::Error::other("zsh registration has no first-line newline"))?;
buf.extend_from_slice(&clap[..=nl]); writeln!(buf, "{MARKER}")?;
writeln!(buf, "{BINDING_PLACEHOLDER}")?;
buf.extend_from_slice(&clap[nl + 1..]);
writeln!(
buf,
"() {{\n setopt localoptions noksharrays\n if (( ${{funcstack[(Ie)_hf2q]}} > 0 )); then\n _clap_dynamic_completer_hf2q\n fi\n}}"
)?;
}
finalize_registration_binding(buf)
}
fn registration_binding(registration: &[u8]) -> String {
use sha2::{Digest as _, Sha256};
let digest = Sha256::digest(registration);
format!("{BINDING_PREFIX}{digest:x}")
}
fn finalize_registration_binding(mut artifact: Vec<u8>) -> std::io::Result<Vec<u8>> {
let placeholder = BINDING_PLACEHOLDER.as_bytes();
let matches = artifact
.windows(placeholder.len())
.enumerate()
.filter_map(|(index, candidate)| (candidate == placeholder).then_some(index))
.collect::<Vec<_>>();
let [offset] = matches.as_slice() else {
return Err(std::io::Error::other(
"generated completion artifact must contain one binding placeholder",
));
};
let binding = registration_binding(&artifact);
debug_assert_eq!(binding.len(), placeholder.len());
artifact[*offset..*offset + placeholder.len()].copy_from_slice(binding.as_bytes());
Ok(artifact)
}
fn registration_binding_line<'a>(bytes: &'a [u8], shell: &Shell) -> Option<&'a str> {
let line = bytes
.split(|byte| *byte == b'\n')
.nth(shell.marker_line + 1)?;
let line = std::str::from_utf8(line.strip_suffix(b"\r").unwrap_or(line)).ok()?;
line.starts_with(BINDING_PREFIX).then_some(line)
}
fn is_hf2q_managed(bytes: &[u8], marker_line: usize) -> bool {
let Some(line) = bytes.split(|&b| b == b'\n').nth(marker_line) else {
return false;
};
let line = line.strip_suffix(b"\r").unwrap_or(line);
line.starts_with(MARKER_PREFIX.as_bytes())
}
fn is_exact_regular_registration(path: &Path, desired: &[u8]) -> bool {
let Ok(metadata) = std::fs::symlink_metadata(path) else {
return false;
};
if !metadata.file_type().is_file() {
return false;
}
std::fs::read(path).is_ok_and(|bytes| bytes == desired)
}
pub(super) fn atomic_replace_with_hook<F>(
dir: &Path,
target: &Path,
data: &[u8],
mode: u32,
expected: &ExpectedTarget,
temp_label: &str,
before_commit: F,
) -> std::io::Result<()>
where
F: FnOnce(),
{
let mut opened = None;
for attempt in 0..64_u8 {
let path = dir.join(format!(
".hf2q-{temp_label}.{}.{attempt}.tmp",
std::process::id()
));
match OpenOptions::new().write(true).create_new(true).open(&path) {
Ok(file) => {
opened = Some((path, file));
break;
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(error) => return Err(error),
}
}
let Some((temp, mut file)) = opened else {
return Err(std::io::Error::other("no unique temporary filename"));
};
let result = (|| {
file.set_permissions(fs::Permissions::from_mode(mode))?;
file.write_all(data)?;
file.sync_all()?;
drop(file);
before_commit();
match expected {
ExpectedTarget::Absent => rename_no_replace(&temp, target),
ExpectedTarget::Regular { .. } => {
expected.revalidate(target)?;
fs::rename(&temp, target)
}
}
})();
if result.is_err() {
let _ = fs::remove_file(&temp);
}
result
}
fn c_path(path: &Path) -> std::io::Result<CString> {
CString::new(path.as_os_str().as_bytes())
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "path contains NUL"))
}
#[cfg(target_os = "linux")]
fn rename_no_replace_platform(from: &Path, to: &Path) -> std::io::Result<()> {
let from = c_path(from)?;
let to = c_path(to)?;
let result = unsafe {
libc::renameat2(
libc::AT_FDCWD,
from.as_ptr(),
libc::AT_FDCWD,
to.as_ptr(),
libc::RENAME_NOREPLACE,
)
};
(result == 0)
.then_some(())
.ok_or_else(std::io::Error::last_os_error)
}
#[cfg(target_os = "macos")]
fn rename_no_replace_platform(from: &Path, to: &Path) -> std::io::Result<()> {
let from = c_path(from)?;
let to = c_path(to)?;
let result = unsafe {
libc::renameatx_np(
libc::AT_FDCWD,
from.as_ptr(),
libc::AT_FDCWD,
to.as_ptr(),
libc::RENAME_EXCL,
)
};
(result == 0)
.then_some(())
.ok_or_else(std::io::Error::last_os_error)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn rename_no_replace_platform(_from: &Path, _to: &Path) -> std::io::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"no platform no-replace rename",
))
}
fn rename_no_replace(from: &Path, to: &Path) -> std::io::Result<()> {
match rename_no_replace_platform(from, to) {
Ok(()) => Ok(()),
Err(error)
if error.raw_os_error().is_some_and(|code| {
code == libc::ENOSYS || code == libc::EINVAL || code == libc::ENOTSUP
}) =>
{
fs::hard_link(from, to)?;
fs::remove_file(from)
}
Err(error) => Err(error),
}
}
#[cfg(test)]
#[path = "completion_install_tests.rs"]
mod tests;