use std::ffi::CString;
use std::fs;
use std::io::{self, Read, Write};
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::os::unix::io::{AsRawFd, RawFd};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Mutex;
use std::time::UNIX_EPOCH;
use crate::init::zleentry;
use crate::params::getsparam_u;
use crate::ported::builtin::{BUILTINS, SFCONTEXT, STOPMSG};
use crate::ported::compat::u9_iswprint;
use crate::ported::hashnameddir::{nameddirtab, removenameddirnode};
use crate::ported::hashtable::shfunctab_lock;
use crate::ported::hist::{bangchar, chrealpath};
use crate::ported::init::SHTTY;
use crate::ported::modules::clone::{coprocin, coprocout, mypgrp};
use crate::{DPUTS, DPUTS1};
use libc::{
S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP,
S_IXOTH, S_IXUSR,
};
use crate::ported::lex::{lineno, untokenize};
use crate::ported::options::{dosetopt, opt_state_set};
use crate::ported::params::{
assignsparam, convbase as convbase_param, getaparam, getsparam, homesetfn, ifsgetfn, ifssetfn,
isident, locallevel as LOCALLEVEL, setaparam, setiparam, wordcharsgetfn, wordcharssetfn,
};
use crate::ported::signals::{queue_signals, unqueue_signals};
use crate::ported::string::dupstrpfx;
use crate::ported::zsh_h::{
dirsav, hashnode, interact, isset, jobbing, nameddir, opt_name, shfunc, unset, Dash, Marker,
Meta, Nularg, Pound, Snull, AUTONAMEDIRS, BANGHIST, BEEP, CHASELINKS, CSHJUNKIEQUOTES,
DEFAULT_IFS, DVORAK, EMULATE_KSH, EMULATE_SH, EMULATION, EXTENDEDGLOB, FDT_EXTERNAL, FDT_FLOCK,
FDT_FLOCK_EXEC, FDT_INTERNAL, FDT_MODULE, FDT_UNUSED, GLOBDOTS, HISTFLAG_NOEXEC,
LAST_NORMAL_TOK, MAGICEQUALSUBST, MULTIBYTE, ND_NOABBREV, ND_USERNAME, NICEFLAG_HEAP,
NICEFLAG_NODUP, NICEFLAG_QUOTE, OCTALZEROES, PATCHARS, POSIXIDENTIFIERS, PRINTEIGHTBIT,
QT_BACKSLASH, QT_BACKSLASH_PATTERN, QT_BACKSLASH_SHOWNULL, QT_BACKTICK, QT_DOLLARS, QT_DOUBLE,
QT_NONE, QT_SINGLE, QT_SINGLE_OPTIONAL, RCQUOTES, RMSTARWAIT, SFC_SUBST, SHINSTDIN, SPECCHARS,
XTRACE, ZLE_CMD_TRASH,
};
use crate::ported::zsh_system_h::DEFAULT_WORDCHARS;
use crate::ported::ztype_h::{
imeta, itok, iwsep, IALNUM, IALPHA, IBLANK, ICNTRL, IDIGIT, IIDENT, IMETA, INBLANK, INULL,
IPATTERN, ISEP, ISPECIAL, ITOK, IUSER, IWORD, IWSEP, TYPTAB, TYPTAB_FLAGS, ZISPRINT,
ZTF_BANGCHAR, ZTF_INIT, ZTF_INTERACT, ZTF_SP_COMMA,
};
pub fn set_widearray(mb_array: &str) -> Vec<char> {
let mut bytes = mb_array.as_bytes().to_vec();
unmetafy(&mut bytes);
match std::str::from_utf8(&bytes) {
Ok(s) => s.chars().collect(),
Err(_) => Vec::new(),
}
}
fn zwarning(cmd: Option<&str>, msg: &str) {
if unsafe { libc::isatty(2) } != 0 {
let _ = zleentry(
ZLE_CMD_TRASH, );
}
let scriptname = scriptname_lock().lock().unwrap().clone();
let argzero = argzero_lock().lock().unwrap().clone();
let locallevel = LOCALLEVEL.load(Ordering::Relaxed);
let prefix: String = scriptname.or(argzero).unwrap_or_default();
let stderr_handle = io::stderr();
let mut stderr_lock = stderr_handle.lock();
if let Some(cmd) = cmd {
if unset(SHINSTDIN) || locallevel != 0 {
let _ = nicezputs(&prefix, &mut stderr_lock); let _ = stderr_lock.write_all(b":");
}
let _ = nicezputs(cmd, &mut stderr_lock); let _ = stderr_lock.write_all(b":");
} else {
let to_emit = if isset(SHINSTDIN) && locallevel == 0 {
"zsh"
} else {
prefix.as_str()
};
let _ = nicezputs(to_emit, &mut stderr_lock); let _ = stderr_lock.write_all(b":");
}
let lineno = lineno() as i32;
if (unset(SHINSTDIN) || locallevel != 0) && lineno != 0 {
let _ = stderr_lock.write_all(format!("{}: ", lineno).as_bytes());
} else {
let _ = stderr_lock.write_all(b" ");
}
let _ = stderr_lock.write_all(msg.as_bytes());
let _ = stderr_lock.write_all(b"\n");
let _ = stderr_lock.flush();
}
pub fn zerr(msg: &str) {
let noerrs = *noerrs_lock().lock().unwrap();
if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
if noerrs < 2 {
errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); }
return; }
errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); zwarning(None, msg); }
pub fn zerrnam(cmd: &str, msg: &str) {
let noerrs = *noerrs_lock().lock().unwrap();
if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
return;
}
errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); zwarning(Some(cmd), msg); }
pub fn zwarn(msg: &str) {
let noerrs = *noerrs_lock().lock().unwrap();
if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
return;
}
zwarning(None, msg); }
pub fn zwarnnam(cmd: &str, msg: &str) {
let noerrs = *noerrs_lock().lock().unwrap();
if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
return;
}
zwarning(Some(cmd), msg); }
pub fn dputs(msg: &str) {
let log_file = getsparam_u("ZSH_DEBUG_LOG"); let opened = log_file.as_ref().and_then(|p| {
fs::OpenOptions::new()
.create(true)
.append(true)
.open(p)
.ok()
});
let lineno = lineno() as i32;
let shinstdin = isset(SHINSTDIN);
let locallevel = LOCALLEVEL.load(Ordering::Relaxed);
let prefix = if (!shinstdin || locallevel != 0) && lineno != 0 {
format!("{}: ", lineno)
} else {
String::new()
};
let line = format!("{}{}\n", prefix, msg);
if let Some(mut f) = opened {
let _ = f.write_all(line.as_bytes()); } else {
let _ = io::stderr().write_all(line.as_bytes()); }
}
pub fn zz_plural_z_alpha() {}
pub fn zerrmsg(msg: &str, errno: Option<i32>) {
let lineno = lineno() as i32;
let locallevel = LOCALLEVEL.load(Ordering::Relaxed);
if (unset(SHINSTDIN) || locallevel != 0) && lineno != 0 {
eprint!("{}: ", lineno);
} else {
eprint!(" ");
}
if let Some(e) = errno {
eprintln!("{}: {}", msg, io::Error::from_raw_os_error(e));
} else {
eprintln!("{}", msg);
}
}
pub fn zsetupterm() {
static TERM_COUNT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
let tc = TERM_COUNT.load(Ordering::Relaxed);
DPUTS!(tc < 0, "inconsistent term_count and/or cur_term"); TERM_COUNT.fetch_add(1, Ordering::Relaxed); }
pub fn zdeleteterm() {
static TERM_COUNT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
let tc = TERM_COUNT.load(Ordering::Relaxed);
DPUTS!(tc < 1, "inconsistent term_count and/or cur_term"); if tc > 0 {
TERM_COUNT.fetch_sub(1, Ordering::Relaxed); }
}
pub fn putraw(c: char) -> i32 {
print!("{}", c); 0 }
pub fn putshout(c: char) -> i32 {
print!("{}", c); 0 }
pub fn nicechar_sel(c: char, quotable: bool) -> String {
let mut c = (c as u32) & 0xff;
let mut out = String::new();
if !ZISPRINT(c as u8) {
if c & 0x80 != 0 {
if isset(PRINTEIGHTBIT) { } else {
out.push_str("\\M-"); c &= 0x7f; if ZISPRINT(c as u8) {
if let Some(ch) = char::from_u32(c) {
out.push(ch);
}
return out;
}
}
}
if c == 0x7f {
out.push_str(if quotable { "\\C-" } else { "^" }); c = b'?' as u32;
} else if c == b'\n' as u32 {
out.push('\\');
c = b'n' as u32;
} else if c == b'\t' as u32 {
out.push('\\');
c = b't' as u32;
} else if c < 0x20 {
out.push_str(if quotable { "\\C-" } else { "^" }); c += 0x40;
}
}
if let Some(ch) = char::from_u32(c) {
out.push(ch);
}
out
}
pub fn nicechar(c: char) -> String {
nicechar_sel(c, false) }
pub fn is_nicechar(c: char) -> bool {
let cu = (c as u32) & 0xff;
if ZISPRINT(cu as u8) {
return false;
}
if (cu & 0x80) != 0 {
return !isset(PRINTEIGHTBIT);
}
cu == 0x7f || cu == b'\n' as u32 || cu == b'\t' as u32 || cu < 0x20
}
pub fn mb_charinit() {
}
pub fn wcs_nicechar_sel(
c: char,
widthp: Option<&mut usize>,
swidep: Option<&mut usize>,
quotable: bool,
) -> String {
let cv = c as u32;
let print_eightbit = isset(PRINTEIGHTBIT);
let is_printable = u9_iswprint(c);
let buf: String;
if !is_printable && (cv < 0x80 || !print_eightbit) {
if cv == 0x7f {
buf = if quotable {
"\\C-?".to_string()
} else {
"^?".to_string()
};
if let Some(wp) = widthp {
*wp = buf.chars().count();
}
if let Some(sp) = swidep {
*sp = buf.len();
}
return buf;
} else if c == '\n' {
buf = "\\n".to_string();
if let Some(wp) = widthp {
*wp = 2;
}
if let Some(sp) = swidep {
*sp = buf.len();
}
return buf;
} else if c == '\t' {
buf = "\\t".to_string();
if let Some(wp) = widthp {
*wp = 2;
}
if let Some(sp) = swidep {
*sp = buf.len();
}
return buf;
} else if cv < 0x20 {
let cc = (cv + 0x40) as u8 as char;
buf = if quotable {
format!("\\C-{}", cc)
} else {
format!("^{}", cc)
};
if let Some(wp) = widthp {
*wp = buf.chars().count();
}
if let Some(sp) = swidep {
*sp = buf.len();
}
return buf;
}
} else if cv < 0x80 {
buf = c.to_string();
if let Some(wp) = widthp {
*wp = 1;
}
if let Some(sp) = swidep {
*sp = buf.len();
}
return buf;
}
if u9_iswprint(c) {
buf = c.to_string();
let wcw = zwcwidth(c) as usize;
if let Some(wp) = widthp {
*wp = wcw;
}
if let Some(sp) = swidep {
*sp = buf.len();
}
return buf;
}
if cv >= 0x10000 {
buf = format!("\\U{:08x}", cv);
if let Some(wp) = widthp {
*wp = 10;
}
if let Some(sp) = swidep {
*sp = buf.len();
}
buf
} else if cv >= 0x100 {
buf = format!("\\u{:04x}", cv);
if let Some(wp) = widthp {
*wp = 6;
}
if let Some(sp) = swidep {
*sp = buf.len();
}
buf
} else {
buf = nicechar_sel(c, quotable);
if let Some(wp) = widthp {
*wp = ztrlen(&buf);
}
if let Some(sp) = swidep {
*sp = buf.len();
}
buf
}
}
pub fn wcs_nicechar(c: char, widthp: Option<&mut usize>, swidep: Option<&mut usize>) -> String {
wcs_nicechar_sel(c, widthp, swidep, false) }
pub fn is_wcs_nicechar(c: char) -> bool {
let cv = c as u32;
let printable = !c.is_control() && cv >= 0x20;
let print_eight = isset(PRINTEIGHTBIT); if !printable && (cv < 0x80 || !print_eight) {
if cv == 0x7f || c == '\n' || c == '\t' || cv < 0x20 {
return true;
}
if cv >= 0x80 {
return cv >= 0x100 || is_nicechar(c); }
}
false }
pub fn zwcwidth(wc: char) -> usize {
if !isset(MULTIBYTE) {
return 1; }
unicode_width::UnicodeWidthChar::width(wc).unwrap_or(1) }
pub fn pathprog(prog: &str) -> Option<PathBuf> {
if prog.contains('/') {
let p = PathBuf::from(prog);
return if p.exists() { Some(p) } else { None };
}
if let Some(path_var) = getsparam("PATH") {
for dir in path_var.split(':') {
let full_path = PathBuf::from(dir).join(prog);
let unmeta_path = unmeta(full_path.to_str().unwrap_or("")); if let Ok(meta) = fs::metadata(&unmeta_path) {
if meta.is_file() {
return Some(PathBuf::from(unmeta_path));
}
}
}
}
None
}
pub fn findpwd(s: &str) -> Option<String> {
if s.starts_with('/') {
return xsymlink(s); }
let pwd = getsparam("PWD")
.or_else(|| {
std::env::current_dir()
.ok()
.map(|p| p.to_string_lossy().into_owned())
})
.unwrap_or_default(); let prefix: &str = if pwd.len() > 1 { &pwd } else { "" }; let combined = format!("{}/{}", prefix, s); xsymlink(&combined) }
pub(crate) fn ispwd(pwd: &str) -> bool {
if !pwd.starts_with('/') {
return false;
}
let pwd_meta = match fs::metadata(pwd) {
Ok(m) => m,
Err(_) => return false,
};
let dot_meta = match fs::metadata(".") {
Ok(m) => m,
Err(_) => return false,
};
if pwd_meta.dev() != dot_meta.dev() || pwd_meta.ino() != dot_meta.ino() {
return false;
}
for comp in pwd.split('/') {
if comp == "." || comp == ".." {
return false;
}
}
true
}
pub fn slashsplit(s: &str) -> Vec<String> {
if s.is_empty() {
return Vec::new();
}
let mut result = Vec::new();
let mut rest = s;
while let Some(pos) = rest.find('/') {
result.push(rest[..pos].to_string()); rest = &rest[pos..];
while let Some(rest_after) = rest.strip_prefix('/') {
rest = rest_after;
}
if rest.is_empty() {
return result;
}
}
result.push(rest.to_string());
result
}
pub fn xsymlinks(s: &str) -> io::Result<String> {
if s.is_empty() {
return Ok(String::new());
}
let path = if !s.starts_with('/') {
let cwd = std::env::current_dir()?;
format!("{}/{}", cwd.display(), s)
} else {
s.to_string()
};
let components: Vec<&str> = path.split('/').collect();
let mut xbuf = String::new();
for comp in components {
match comp {
"" | "." => continue, ".." => {
if xbuf == "/" || xbuf.is_empty() {
continue;
}
if let Some(pos) = xbuf.rfind('/') {
xbuf.truncate(pos);
}
}
c => {
let candidate = format!("{}/{}", xbuf, c); #[cfg(unix)]
{
match fs::read_link(&candidate) {
Ok(target) => {
let t = target.to_string_lossy().into_owned();
if t.starts_with('/') {
xbuf = t; } else {
xbuf = format!("{}/{}", xbuf, t); }
continue;
}
Err(_) => {
xbuf = candidate; }
}
}
#[cfg(not(unix))]
{
xbuf = candidate;
}
}
}
}
if xbuf.is_empty() {
Ok(if path.starts_with('/') {
"/".to_string()
} else {
".".to_string()
})
} else {
Ok(xbuf)
}
}
pub fn xsymlink(path: &str) -> Option<String> {
if !path.starts_with('/') {
return None;
}
match chrealpath(path, b'P', false) {
Some(r) => Some(r), None => {
zwarn("path expansion failed, using root directory"); Some("/".to_string()) }
}
}
pub fn print_if_link(s: &str, all: bool) {
if !s.starts_with('/') {
return;
}
if all {
let mut start = s.to_string();
loop {
match xsymlinks(&start) {
Ok(target) if !target.is_empty() && target != start => {
print!(" -> "); print!("{}", if target.is_empty() { "/" } else { &target }); start = target; }
_ => break, }
}
} else {
DPUTS1!(
s.len() >= libc::PATH_MAX as usize, "path longer than PATH_MAX: {}",
s );
let s_at_entry = s.to_string(); let mut resolved = s.to_string(); if let Some(r) = chrealpath(&resolved, b'P', false) {
resolved = r;
if resolved != s_at_entry {
print!(" -> "); print!("{}", if resolved.is_empty() { "/" } else { &resolved });
}
}
}
let _ = io::stdout().flush();
}
pub fn fprintdir(s: &str) -> String {
match finddir(s) {
None => unmeta(s), Some(rendered) => rendered, }
}
pub fn substnamedir(s: &str) -> String {
let home = getsparam("HOME").unwrap_or_default(); if !home.is_empty() && home.len() > 1 && s.starts_with(&home) {
let rest = &s[home.len()..];
if rest.is_empty() || rest.starts_with('/') {
return format!("~{}", quotestring(rest, QT_BACKSLASH)); }
}
if let Some((name, rest)) = finddir_scan(s) {
return format!("~{}{}", name, quotestring(&rest, QT_BACKSLASH)); }
quotestring(s, QT_BACKSLASH) }
pub fn get_username() -> String {
static CACHE: Mutex<Option<(u32, String)>> = Mutex::new(None);
let current_uid = unsafe { libc::getuid() };
let mut guard = CACHE.lock().unwrap();
if let Some((uid, name)) = &*guard {
if *uid == current_uid {
return name.clone();
}
}
let name = unsafe {
let pw = libc::getpwuid(current_uid);
if pw.is_null() {
String::new() } else {
let raw = std::ffi::CStr::from_ptr((*pw).pw_name)
.to_string_lossy()
.into_owned();
metafy(&raw) }
};
*guard = Some((current_uid, name.clone()));
name
}
pub fn finddir_scan(path: &str) -> Option<(String, String)> {
let table = nameddirtab().lock().ok()?;
let mut best: Option<(String, String, usize)> = None;
for (name, nd) in table.iter() {
if path.starts_with(nd.dir.as_str()) {
let len = nd.dir.len();
let rest = &path[len..];
if (rest.is_empty() || rest.starts_with('/'))
&& best.as_ref().map_or(true, |b| len > b.2)
{
best = Some((name.clone(), rest.to_string(), len));
}
}
}
best.map(|(n, r, _)| (n, r))
}
pub fn finddir(path: &str) -> Option<String> {
let _default_pm = crate::ported::zsh_h::param::default();
let home = crate::ported::params::homegetfn(&_default_pm); if !home.is_empty() && home.len() > 1 && path.starts_with(&home) {
let rest = &path[home.len()..];
if rest.is_empty() || rest.starts_with('/') {
return Some(format!("~{}", rest));
}
}
if let Some((name, rest)) = finddir_scan(path) {
return Some(format!("~{}{}", name, rest));
}
if let Some(reply) = subst_string_by_hook("zsh_directory_name", Some("d"), path) {
if reply.len() >= 2 {
if let Ok(len) = reply[1].parse::<usize>() {
if len <= path.len() {
let prefix = &path[..len];
let _ = prefix;
return Some(format!("~[{}]{}", reply[0], &path[len..]));
}
}
}
}
None }
pub fn adduserdir(name: &str, dir: &str, flags: i32, always: bool) {
if !interact() {
return;
} if let Ok(t) = nameddirtab().lock() {
if (flags & ND_USERNAME) != 0 && t.contains_key(name) {
return;
}
if !always && !isset(AUTONAMEDIRS) && !t.contains_key(name) {
return;
}
}
if dir.is_empty() || !dir.starts_with('/') || dir.len() >= libc::PATH_MAX as usize
{
let _ = removenameddirnode(name); return;
}
let mut trimmed = dir.trim_end_matches('/').to_string(); if trimmed.is_empty() {
trimmed = dir.to_string(); }
let final_flags = if name == "PWD" || name == "OLDPWD" {
flags | ND_NOABBREV
} else {
flags
};
let nd = nameddir {
node: hashnode {
next: None,
nam: name.to_string(),
flags: final_flags,
},
dir: trimmed,
diff: 0,
};
crate::ported::hashnameddir::addnameddirnode(name, nd);
}
pub fn getnameddir(name: &str) -> Option<String> {
if let Ok(t) = nameddirtab().lock() {
if let Some(nd) = t.get(name) {
return Some(nd.dir.clone());
}
}
if let Some(s) = getsparam(name) {
if s.starts_with('/') {
adduserdir(name, &s, 0, true); return Some(s);
}
}
#[cfg(unix)]
{
let cn = CString::new(name).ok()?;
let pw = unsafe { libc::getpwnam(cn.as_ptr()) };
if !pw.is_null() {
let raw_dir = unsafe {
std::ffi::CStr::from_ptr((*pw).pw_dir)
.to_string_lossy()
.into_owned()
};
let dir = if isset(CHASELINKS) {
xsymlink(&raw_dir).unwrap_or(raw_dir)
} else {
raw_dir
};
adduserdir(name, &dir, ND_USERNAME, true); return Some(dir);
}
}
None
}
pub fn dircmp(s: &str, t: &str) -> bool {
let s = s.trim_end_matches('/');
let t = t.trim_end_matches('/');
s == t
}
pub fn addprepromptfn(func: fn()) {
PREPROMPT_FNS.lock().unwrap().push(func);
}
pub fn delprepromptfn(func: fn()) {
let mut list = PREPROMPT_FNS.lock().unwrap();
if let Some(pos) = list.iter().position(|f| *f as usize == func as usize) {
list.remove(pos);
}
}
pub fn addtimedfn(func: fn(), when: i64) {
let mut list = TIMED_FNS.lock().unwrap(); let tfdat: (i64, fn()) = (when, func); if list.is_empty() {
list.push(tfdat); return;
}
if list.is_empty() {
list.push(tfdat); return; }
let mut idx: usize = 0; loop {
let next = idx + 1; if next >= list.len() {
list.push(tfdat); return; }
let tfdat2_when = list[next].0; if when < tfdat2_when {
list.insert(next, tfdat); return; }
idx = next; }
}
pub fn deltimedfn(func: fn()) {
let mut list = TIMED_FNS.lock().unwrap();
if let Some(pos) = list.iter().position(|(_, f)| *f as usize == func as usize) {
list.remove(pos);
}
}
pub fn callhookfunc(name: &str, lnklst: Option<&[String]>, arrayp: i32, retval: *mut i32) -> i32 {
let mut stat: i32 = 1; let mut ret: i32 = 0;
let mk_args = |fname: &str| -> Vec<String> {
let mut v: Vec<String> = vec![fname.to_string()];
if let Some(extra) = lnklst {
v.extend_from_slice(extra);
}
v
};
let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
.read()
.ok()
.and_then(|t| t.get(name).cloned());
if let Some(mut shf) = shf_clone {
let args = mk_args(name);
let name_for_body = name.to_string();
let body_args = args.clone();
let body_runner = move || -> i32 {
crate::ported::exec_hooks::run_function_body(&name_for_body, &body_args[1..])
.unwrap_or(0)
};
ret = crate::ported::exec::doshfunc(&mut shf, args, true, body_runner);
stat = 0; }
if arrayp != 0 {
let arr_name = format!("{}_functions", name); let arr = crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| t.get(&arr_name).and_then(|p| p.u_arr.clone()))
.unwrap_or_default(); for fn_name in arr {
let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
.read()
.ok()
.and_then(|t| t.get(&fn_name).cloned());
if let Some(mut shf) = shf_clone {
let args = mk_args(&fn_name);
let name_for_body = fn_name.clone();
let body_args = args.clone();
let body_runner = move || -> i32 {
crate::ported::exec_hooks::run_function_body(&name_for_body, &body_args[1..])
.unwrap_or(0)
};
ret = crate::ported::exec::doshfunc(&mut shf, args, true, body_runner);
stat = 0; }
}
}
if !retval.is_null() {
unsafe {
*retval = ret;
}
}
let _ = ret;
stat }
pub fn preprompt() {
static LAST_PERIODIC: AtomicI64 = AtomicI64::new(0);
static LAST_MAILCHECK: AtomicI64 = AtomicI64::new(0);
let period = crate::ported::params::getiparam("PERIOD");
let mailcheck = crate::ported::params::getiparam("MAILCHECK");
crate::ported::signals_h::winch_unblock();
crate::ported::signals_h::winch_block();
if !crate::ported::zsh_h::isset(crate::ported::zsh_h::NOTIFY) {
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let long_list = crate::ported::zsh_h::isset(crate::ported::zsh_h::LONGLISTJOBS);
for i in 1..guard.len() {
if (guard[i].stat & crate::ported::zsh_h::STAT_CHANGED) != 0 {
let s = crate::ported::jobs::printjob(&guard[i], i, long_list, None, None);
if !s.is_empty() {
eprint!("{}", s);
}
}
}
}
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
return;
}
callhookfunc("precmd", None, 1, std::ptr::null_mut());
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
return;
}
if period > 0 {
let now = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
if now > LAST_PERIODIC.load(Ordering::Relaxed) + period
&& callhookfunc("periodic", None, 1, std::ptr::null_mut()) == 0
{
LAST_PERIODIC.store(now, Ordering::Relaxed);
}
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
return;
}
let currentmailcheck = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
if mailcheck > 0 && (currentmailcheck - LAST_MAILCHECK.load(Ordering::Relaxed)) > mailcheck {
let mailpath = crate::ported::params::getaparam("MAILPATH");
let has_mailpath = mailpath
.as_ref()
.map(|p| !p.is_empty() && p.first().map(|s| !s.is_empty()).unwrap_or(false))
.unwrap_or(false);
if has_mailpath {
let _ = checkmailpath(mailpath.as_deref().unwrap()); } else {
crate::ported::signals::queue_signals(); if let Some(mailfile) = crate::ported::params::getsparam("MAIL") {
if !mailfile.is_empty() {
let x = vec![mailfile]; let _ = checkmailpath(&x); }
}
crate::ported::signals::unqueue_signals(); }
LAST_MAILCHECK.store(currentmailcheck, Ordering::Relaxed); }
let snapshot: Vec<fn()> = PREPROMPT_FNS.lock().unwrap().clone();
for f in snapshot {
f();
}
}
pub fn checkmailpath(paths: &[String]) -> Vec<String> {
let mut messages = Vec::new();
for path in paths {
let (file, msg) = if let Some(pos) = path.find('?') {
(&path[..pos], Some(&path[pos + 1..]))
} else {
(path.as_str(), None)
};
if let Ok(meta) = fs::metadata(file) {
if let Ok(modified) = meta.modified() {
if let Ok(elapsed) = modified.elapsed() {
if elapsed.as_secs() < 60 {
let default_msg = format!("You have new mail in {}", file);
messages.push(msg.unwrap_or(&default_msg).to_string());
}
}
}
}
}
messages
}
pub(crate) fn printprompt4() {
if !isset(XTRACE) {
return;
}
let posix = EMULATION(EMULATE_KSH | EMULATE_SH); let prefix_template = getsparam("PS4")
.or_else(|| getsparam("PROMPT4"))
.unwrap_or_else(|| {
if posix {
"+ ".to_string() } else {
"+%N:%i> ".to_string() }
});
let saved = isset(XTRACE);
opt_state_set(&opt_name(XTRACE), false);
let prefix = crate::prompt::expand_prompt(&prefix_template);
opt_state_set(&opt_name(XTRACE), saved);
eprint!("{}", prefix);
}
pub fn freestr(_a: String) {}
pub fn gettempfile(prefix: Option<&str>) -> Option<(i32, String)> {
#[cfg(unix)]
{
queue_signals(); let old_umask = unsafe { libc::umask(0o177) }; let mut failures = 0; let mut result: Option<(i32, String)> = None;
loop {
let fn_ = match gettempname(prefix, false) {
Some(n) => n,
None => break,
};
let cn = match CString::new(fn_.clone()) {
Ok(c) => c,
Err(_) => break,
};
let fd = unsafe {
libc::open(
cn.as_ptr(),
libc::O_RDWR | libc::O_CREAT | libc::O_EXCL, 0o600 as libc::c_int,
)
};
if fd >= 0 {
result = Some((fd, fn_));
break;
}
let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
if err != libc::EEXIST {
break;
}
failures += 1;
if failures >= 16 {
break;
}
}
unsafe {
libc::umask(old_umask);
} unqueue_signals(); result
}
#[cfg(not(unix))]
{
let _ = prefix;
None
}
}
#[cfg(unix)]
pub fn gettyinfo() -> Option<libc::termios> {
fdgettyinfo(SHTTY.load(Ordering::Relaxed)).ok() }
#[cfg(unix)]
pub fn fdgettyinfo(fd: i32) -> io::Result<libc::termios> {
let mut tio: libc::termios = unsafe { std::mem::zeroed() };
if unsafe { libc::tcgetattr(fd, &mut tio) } == -1 {
Err(io::Error::last_os_error())
} else {
Ok(tio)
}
}
#[cfg(not(unix))]
pub fn fdgettyinfo(_fd: i32) -> std::io::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"no termios",
))
}
#[cfg(unix)]
pub fn settyinfo(ti: &libc::termios) -> bool {
fdsettyinfo(SHTTY.load(Ordering::Relaxed), ti).is_ok() }
#[cfg(unix)]
pub fn fdsettyinfo(SHTTY2: i32, ti: &libc::termios) -> io::Result<()> {
loop {
if unsafe { libc::tcsetattr(SHTTY2, libc::TCSADRAIN, ti) } != -1 {
return Ok(());
}
let err = io::Error::last_os_error();
if err.kind() != io::ErrorKind::Interrupted {
return Err(err);
}
}
}
#[cfg(not(unix))]
pub fn fdsettyinfo(SHTTY: i32, ti: &()) -> std::io::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"no termios",
))
}
pub fn adjustlines() -> usize {
#[cfg(unix)]
{
unsafe {
let mut ws: libc::winsize = std::mem::zeroed();
if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_row > 0 {
return ws.ws_row as usize;
}
}
}
getsparam("LINES")
.and_then(|s| s.parse().ok())
.unwrap_or(24)
}
pub fn adjustcolumns() -> usize {
#[cfg(unix)]
{
unsafe {
let mut ws: libc::winsize = std::mem::zeroed();
if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 {
return ws.ws_col as usize;
}
}
}
getsparam("COLUMNS")
.and_then(|s| s.parse().ok())
.unwrap_or(80)
}
pub fn adjustwinsize(from: i32) -> (usize, usize) {
let getwinsz = ADJUSTWINSIZE_GETWINSZ.load(Ordering::SeqCst);
let mut ttyrows: i32 = 0;
let mut ttycols: i32 = 0;
if getwinsz != 0 || from == 1 {
let shtty = SHTTY.load(Ordering::Relaxed);
if shtty == -1 {
return (adjustcolumns(), adjustlines()); }
#[cfg(unix)]
unsafe {
let mut ws: libc::winsize = std::mem::zeroed();
if libc::ioctl(shtty, libc::TIOCGWINSZ, &mut ws as *mut _) == 0 {
ttyrows = ws.ws_row as i32; ttycols = ws.ws_col as i32; }
}
}
let mut resetzle = 0i32;
match from {
0 | 1 => {
ADJUSTWINSIZE_GETWINSZ.store(0, Ordering::SeqCst); let lines = adjustlines() as i32;
if std::env::var_os("LINES").is_some() {
setiparam("LINES", lines as i64); }
let cols = adjustcolumns() as i32;
if std::env::var_os("COLUMNS").is_some() {
setiparam("COLUMNS", cols as i64); }
ADJUSTWINSIZE_GETWINSZ.store(1, Ordering::SeqCst); }
2 => {
resetzle = adjustlines() as i32; }
3 => {
resetzle = adjustcolumns() as i32; }
_ => {}
}
if from >= 2 && resetzle != 0 {
let _ = ttyrows;
let _ = ttycols;
}
(adjustcolumns(), adjustlines())
}
pub static ADJUSTWINSIZE_GETWINSZ: std::sync::atomic::AtomicI32 =
std::sync::atomic::AtomicI32::new(1);
pub fn check_fd_table(fd: i32) -> bool {
let cur_max = MAX_ZSH_FD.load(Ordering::Relaxed); if fd <= cur_max {
return true; }
if fd < 0 {
return false;
}
{
let mut g = fdtable_lock().lock().unwrap();
if (fd as usize) >= g.len() {
g.resize((fd as usize) + 1, FDT_UNUSED); }
}
MAX_ZSH_FD.store(fd, Ordering::Relaxed); true
}
pub fn movefd(fd: i32) -> i32 {
#[cfg(unix)]
{
let mut fd = fd;
if fd != -1 && fd < 10 {
let fe = unsafe { libc::fcntl(fd, libc::F_DUPFD, 10) }; unsafe { libc::close(fd) }; fd = fe; }
if fd != -1 {
check_fd_table(fd); fdtable_set(fd, FDT_INTERNAL); }
fd
}
#[cfg(not(unix))]
{
fd
}
}
pub fn redup(x: i32, y: i32) -> i32 {
let mut ret = y; #[cfg(unix)]
{
if x < 0 {
zclose(y); } else if x != y {
if unsafe { libc::dup2(x, y) } == -1 {
ret = -1; } else {
check_fd_table(y); let kind_x = fdtable_get(x); let kind_y = if kind_x == FDT_FLOCK || kind_x == FDT_FLOCK_EXEC {
FDT_INTERNAL } else {
kind_x };
fdtable_set(y, kind_y);
}
if fdtable_get(x) == FDT_FLOCK {
FDTABLE_FLOCKS.fetch_sub(1, Ordering::SeqCst); }
zclose(x); }
}
#[cfg(not(unix))]
{
let _ = (x, y);
}
ret }
pub fn addmodulefd(fd: i32, fdt: i32) {
if fd >= 0 {
check_fd_table(fd); fdtable_set(fd, fdt); }
}
pub fn addlockfd(fd: i32, cloexec: bool) {
if cloexec {
if fdtable_get(fd) != FDT_FLOCK {
FDTABLE_FLOCKS.fetch_add(1, Ordering::SeqCst);
}
fdtable_set(fd, FDT_FLOCK);
} else {
fdtable_set(fd, FDT_FLOCK_EXEC);
}
}
pub fn zclose(fd: i32) -> i32 {
if fd >= 0 {
let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed); if fd <= max_fd {
if fdtable_get(fd) == FDT_FLOCK {
FDTABLE_FLOCKS.fetch_sub(1, Ordering::Relaxed);
}
fdtable_set(fd, FDT_UNUSED); let mut m = MAX_ZSH_FD.load(Ordering::Relaxed);
while m > 0 && fdtable_get(m) == FDT_UNUSED {
m -= 1;
}
MAX_ZSH_FD.store(m, Ordering::Relaxed);
if fd == coprocin.load(Ordering::Relaxed) {
coprocin.store(-1, Ordering::Relaxed);
}
if fd == coprocout.load(Ordering::Relaxed) {
coprocout.store(-1, Ordering::Relaxed);
}
}
#[cfg(unix)]
unsafe {
return libc::close(fd);
} #[cfg(not(unix))]
return 0;
}
-1 }
pub fn zcloselockfd(fd: i32) -> i32 {
let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
if fd > max_fd {
return -1;
}
let slot = fdtable_get(fd);
if slot != FDT_FLOCK && slot != FDT_FLOCK_EXEC {
return -1;
}
zclose(fd); 0 }
pub fn gettempname(prefix: Option<&str>, _use_heap: bool) -> Option<String> {
let suffix = if prefix.is_some() {
".XXXXXX"
} else {
"XXXXXX"
}; queue_signals(); let prefix_owned: String = match prefix {
Some(p) => p.to_string(),
None => getsparam("TMPPREFIX")
.unwrap_or_else(|| crate::ported::config_h::DEFAULT_TMPPREFIX.to_string()),
};
let template = format!("{}{}", prefix_owned, suffix); let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let unique = format!("{:x}{:x}", pid, nanos & 0xffffff);
let name = template.replace("XXXXXX", &unique);
unqueue_signals(); Some(name)
}
pub fn has_token(s: &str) -> bool {
s.bytes().any(itok) }
pub fn chuck(s: &mut String, pos: usize) {
if pos < s.len() {
s.remove(pos);
}
}
pub fn tulower(c: char) -> char {
c.to_lowercase().next().unwrap_or(c)
}
pub fn tuupper(c: char) -> char {
c.to_uppercase().next().unwrap_or(c)
}
pub fn ztrncpy(s: &mut String, t: &str, len: usize) {
s.clear(); let take = len.min(t.len()); s.push_str(&t[..take]); }
pub fn strucpy(s: &mut String, t: &str) {
s.push_str(t); }
pub fn struncpy(s: &mut String, t: &str, n: usize) {
let take = n.min(t.len());
s.push_str(&t[..take]);
}
pub fn arrlen<T>(s: &[T]) -> usize {
s.len()
}
pub fn arrlen_ge<T>(arr: &[T], n: usize) -> bool {
arr.len() >= n
}
pub fn arrlen_gt<T>(arr: &[T], n: usize) -> bool {
arr.len() > n
}
pub fn arrlen_le<T>(arr: &[T], n: usize) -> bool {
arr.len() <= n
}
pub fn arrlen_lt<T>(arr: &[T], n: usize) -> bool {
arr.len() < n
}
pub fn skipparens(open: char, close: char, s: &mut &str) -> i32 {
let mut chars = s.char_indices();
match chars.next() {
Some((_, c)) if c == open => {}
_ => return -1,
}
let mut level: i32 = 1;
let mut consumed = open.len_utf8();
for (i, c) in chars {
consumed = i + c.len_utf8();
if c == open {
level += 1; } else if c == close {
level -= 1; }
if level == 0 {
break;
}
}
*s = &s[consumed..];
level
}
pub fn zstrtol(s: &str, base: i32) -> (i64, &str) {
zstrtol_underscore(s, base, false) }
pub fn zstrtol_underscore(s: &str, base: i32, underscore: bool) -> (i64, &str) {
let bytes = s.as_bytes();
let mut i = 0usize;
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
let neg = if i < bytes.len() && (bytes[i] == b'-') {
i += 1;
true
} else if i < bytes.len() && bytes[i] == b'+' {
i += 1;
false
} else {
false
};
let mut base = base;
if base == 0 {
if i >= bytes.len() || bytes[i] != b'0' {
base = 10; } else {
i += 1; if i < bytes.len() && (bytes[i] == b'x' || bytes[i] == b'X') {
base = 16;
i += 1;
} else if i < bytes.len() && (bytes[i] == b'b' || bytes[i] == b'B') {
base = 2;
i += 1;
} else {
base = 8; }
}
}
let inp_idx = i; if base < 2 || base > 36 {
zerr(&format!(
"invalid base (must be 2 to 36 inclusive): {}",
base
)); return (0, s); }
let mut calc: u64 = 0; let mut trunc_idx: Option<usize> = None; if base <= 10 {
let max_d = b'0' + base as u8;
while i < bytes.len() {
let c = bytes[i];
if c >= b'0' && c < max_d {
if trunc_idx.is_none() {
let newcalc = calc
.wrapping_mul(base as u64)
.wrapping_add((c - b'0') as u64);
if newcalc < calc {
trunc_idx = Some(i); } else {
calc = newcalc;
}
}
i += 1;
} else if underscore && c == b'_' {
i += 1;
} else {
break;
}
}
} else {
while i < bytes.len() {
let c = bytes[i];
let digit = if c.is_ascii_digit() {
Some((c - b'0') as u32)
} else if c >= b'a' && c < b'a' + base as u8 - 10 {
Some(((c & 0x1f) + 9) as u32) } else if c >= b'A' && c < b'A' + base as u8 - 10 {
Some(((c & 0x1f) + 9) as u32)
} else if underscore && c == b'_' {
i += 1;
continue;
} else {
None
};
match digit {
Some(d) => {
if trunc_idx.is_none() {
let newcalc = calc.wrapping_mul(base as u64).wrapping_add(d as u64);
if newcalc < calc {
trunc_idx = Some(i); } else {
calc = newcalc;
}
}
i += 1;
}
None => break,
}
}
}
if trunc_idx.is_none() && (calc as i64) < 0 {
let top_bit_only = calc & !(1u64 << 63); if !neg || top_bit_only != 0 {
trunc_idx = Some(i.saturating_sub(1)); calc /= base as u64; }
}
if let Some(t) = trunc_idx {
let digits = t.saturating_sub(inp_idx);
let inp_str = &s[inp_idx..];
zwarn(&format!(
"number truncated after {} digits: {}",
digits, inp_str
)); }
let result = if neg {
-(calc as i64)
} else {
calc as i64
};
(result, &s[i..])
}
pub fn zstrtoul_underscore(s: &str) -> Option<u64> {
let s = s.trim();
let s = s.strip_prefix('+').unwrap_or(s);
let (base, rest) = if s.starts_with("0x") || s.starts_with("0X") {
(16, &s[2..])
} else if s.starts_with("0b") || s.starts_with("0B") {
(2, &s[2..])
} else if s.starts_with('0') && s.len() > 1 && isset(OCTALZEROES)
{
(8, &s[1..])
} else {
(10, s)
};
let rest = rest.replace('_', "");
u64::from_str_radix(&rest, base).ok()
}
pub fn setblock_fd(turnonblocking: bool, fd: i32) -> (bool, libc::c_long) {
#[cfg(unix)]
unsafe {
let mut st: libc::stat = std::mem::zeroed();
if libc::fstat(fd, &mut st) != 0 {
return (false, -1);
}
let mode_bits = st.st_mode as u32;
if (mode_bits & libc::S_IFMT as u32) == libc::S_IFREG as u32 {
return (false, -1); }
let modep = libc::fcntl(fd, libc::F_GETFL, 0) as libc::c_long;
if modep < 0 {
return (false, -1); }
const NONBLOCK: libc::c_long = libc::O_NONBLOCK as libc::c_long;
if !turnonblocking {
if (modep & NONBLOCK) != 0 {
return (true, modep); }
if libc::fcntl(fd, libc::F_SETFL, modep | NONBLOCK) == 0 {
return (true, modep); }
} else {
if (modep & NONBLOCK) != 0 && libc::fcntl(fd, libc::F_SETFL, modep & !NONBLOCK) == 0 {
return (true, modep); }
}
(false, modep)
}
#[cfg(not(unix))]
{
let _ = (turnonblocking, fd);
(false, -1)
}
}
pub fn setblock_stdin() -> i32 {
let (changed, _mode) = setblock_fd(true, 0); changed as i32
}
pub fn read_poll(fd: i32, timeout_us: i64) -> bool {
#[cfg(unix)]
{
let mut fds = [libc::pollfd {
fd: fd as RawFd,
events: libc::POLLIN,
revents: 0,
}];
let timeout_ms = (timeout_us / 1000) as i32;
let result = unsafe { libc::poll(fds.as_mut_ptr(), 1, timeout_ms) };
result > 0 && (fds[0].revents & libc::POLLIN) != 0
}
#[cfg(not(unix))]
{
let _ = (fd, timeout_us);
false
}
}
pub fn timespec_diff_us(t1: &std::time::Instant, t2: &std::time::Instant) -> i64 {
if *t2 > *t1 {
t2.duration_since(*t1).as_micros() as i64
} else {
-(t1.duration_since(*t2).as_micros() as i64)
}
}
pub fn zmonotime(tloc: Option<&mut i64>) -> i64 {
#[cfg(unix)]
{
let mut ts = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
unsafe {
libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts); }
if let Some(t) = tloc {
*t = ts.tv_sec as i64; }
ts.tv_sec as i64 }
#[cfg(not(unix))]
{
let _ = tloc;
0
}
}
pub fn zsleep(us: i64) -> i32 {
let mut sleeptime = libc::timespec {
tv_sec: (us / 1_000_000) as libc::time_t,
tv_nsec: ((us % 1_000_000) * 1000) as libc::c_long,
};
#[cfg(unix)]
{
loop {
let mut rem = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
let ret = unsafe { libc::nanosleep(&sleeptime, &mut rem) }; if ret == 0 {
return 1;
}
let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
if err != libc::EINTR {
return 0; }
sleeptime = rem; }
}
#[cfg(not(unix))]
{
let _ = sleeptime;
std::thread::sleep(std::time::Duration::from_micros(us.max(0) as u64));
1
}
}
pub fn zsleep_random(max_us: i64, end_time: i64) -> i32 {
let now = zmonotime(None); let r16 = unsafe { libc::rand() } & 0xFFFF; let mut r: i64 = (max_us >> 16) * (r16 as i64); while r != 0 && now + (r / 1_000_000) > end_time {
r >>= 1; }
if r != 0 {
zsleep(r) } else {
0 }
}
pub fn checkrmall(s: &str) -> bool {
let shout_is_tty = unsafe { libc::isatty(2) != 0 }; if !shout_is_tty {
return true; }
let s_owned: String; let s_abs: &str = if !s.starts_with('/') {
let pwd = getsparam("PWD").unwrap_or_default(); s_owned = if pwd.len() > 1 {
crate::ported::string::tricat(&pwd, "/", s) } else {
crate::ported::string::dyncat("/", s) };
s_owned.as_str()
} else {
s
};
let max_count: i32 = 100; let mut count: i32 = 0; let ignoredots = !isset(GLOBDOTS); if let Ok(mut dir) = fs::read_dir(s_abs) {
while let Some(fname) = zreaddir(&mut dir, 1) {
if ignoredots && fname.starts_with('.') {
continue; }
count += 1; if count > max_count {
break; }
}
}
let stderr_h = io::stderr();
let mut stderr_w = stderr_h.lock();
if count > max_count {
let _ = write!(
stderr_w,
"zsh: sure you want to delete more than {} files in ",
max_count
); } else if count == 1 {
let _ = write!(stderr_w, "zsh: sure you want to delete the only file in ");
} else if count > 0 {
let _ = write!(
stderr_w,
"zsh: sure you want to delete all {} files in ",
count
); } else {
let _ = write!(stderr_w, "zsh: sure you want to delete all the files in ");
}
let _ = nicezputs(s_abs, &mut stderr_w); if isset(RMSTARWAIT) {
let _ = write!(stderr_w, "? (waiting ten seconds)"); let _ = stderr_w.flush(); drop(stderr_w); zbeep(); std::thread::sleep(std::time::Duration::from_secs(10)); let _ = writeln!(io::stderr()); } else {
drop(stderr_w);
}
if errflag.load(Ordering::Relaxed) != 0 {
return false; }
let _ = io::stderr().write_all(b" [yn]? "); let _ = io::stderr().flush(); zbeep(); getquery(Some("ny"), 1) == b'y' as i32 }
pub fn read_loop(fd: i32, buf: &mut [u8]) -> io::Result<usize> {
#[cfg(unix)]
{
let mut total = 0;
while total < buf.len() {
let n = unsafe {
libc::read(
fd,
buf[total..].as_mut_ptr() as *mut libc::c_void,
buf.len() - total,
)
};
if n <= 0 {
if n < 0 {
let e = io::Error::last_os_error();
if e.kind() == io::ErrorKind::Interrupted {
continue; }
let shtty = SHTTY.load(Ordering::Relaxed);
if fd != shtty {
zwarn(
&format!("read failed: {}", e),
);
}
return Err(e);
}
break;
}
total += n as usize;
}
Ok(total)
}
#[cfg(not(unix))]
{
let _ = (fd, buf);
Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
}
}
pub fn write_loop(fd: i32, buf: &[u8]) -> io::Result<usize> {
#[cfg(unix)]
{
let mut total = 0;
while total < buf.len() {
let n = unsafe {
libc::write(
fd,
buf[total..].as_ptr() as *const libc::c_void,
buf.len() - total,
)
};
if n <= 0 {
if n < 0 {
let e = io::Error::last_os_error();
if e.kind() == io::ErrorKind::Interrupted {
continue; }
let shtty = SHTTY.load(Ordering::Relaxed);
if fd != shtty {
zwarn(
&format!("write failed: {}", e),
);
}
return Err(e);
}
break;
}
total += n as usize;
}
Ok(total)
}
#[cfg(not(unix))]
{
let _ = (fd, buf);
Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
}
}
pub fn read1char(echo: i32) -> i32 {
#[cfg(unix)]
{
let shtty = SHTTY.load(Ordering::Relaxed);
if shtty < 0 {
return -1;
}
let mut c: u8 = 0;
loop {
let rc = unsafe { libc::read(shtty, &mut c as *mut u8 as *mut libc::c_void, 1) };
if rc == 1 {
break;
}
let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
if err != libc::EINTR {
return -1;
}
if errflag.load(Ordering::Relaxed) != 0 {
return -1;
}
}
if echo != 0 {
let _ = write_loop(shtty, &[c]); }
c as i32
}
#[cfg(not(unix))]
{
let _ = echo;
-1
}
}
pub fn noquery(purge: bool) -> i32 {
let mut val: libc::c_int = 0; #[cfg(unix)]
{
let shtty = SHTTY.load(Ordering::Relaxed); if shtty == -1 {
return 0;
}
unsafe {
libc::ioctl(shtty, libc::FIONREAD, &mut val as *mut libc::c_int);
}
if purge {
let mut c: u8 = 0;
for _ in 0..val {
let _ = unsafe {
libc::read(shtty, &mut c as *mut u8 as *mut libc::c_void, 1)
};
}
}
}
val }
pub fn getquery(valid_chars: Option<&str>, purge: i32) -> i32 {
let mut c: i32;
let mut d: i32;
let mut nl: i32 = 0;
let term: String = getsparam("TERM").unwrap_or_default();
let isem: bool = term == "emacs";
let mut ti: libc::termios;
attachtty(mypgrp.load(Ordering::Relaxed));
ti = match gettyinfo() {
Some(t) => t,
None => return -1,
};
#[cfg(unix)]
{
ti.c_lflag &= !(libc::ECHO); if !isem {
ti.c_lflag &= !(libc::ICANON); ti.c_cc[libc::VMIN] = 1; ti.c_cc[libc::VTIME] = 0; }
}
settyinfo(&ti);
if noquery(purge != 0) != 0 {
if !isem {
if let Some(saved) = gettyinfo() {
settyinfo(&saved);
}
}
let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"n\n"); return b'n' as i32; }
c = -1;
loop {
let cc = read1char(0); if cc < 0 {
c = cc;
break;
}
c = cc;
if c == b'Y' as i32 {
c = b'y' as i32; } else if c == b'N' as i32 {
c = b'n' as i32; }
if valid_chars.is_none() {
break; }
if c == b'\n' as i32 {
c = valid_chars.unwrap().bytes().next().unwrap_or(b'\n') as i32;
nl = 1; break; }
if valid_chars.unwrap().bytes().any(|b| b as i32 == c) {
nl = 1; break; }
zbeep(); }
if c >= 0 {
let buf = [c as u8]; let _ = write_loop(SHTTY.load(Ordering::Relaxed), &buf); }
if nl != 0 {
let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"\n"); }
if isem {
if c != b'\n' as i32 {
loop {
d = read1char(1);
if d < 0 || d == b'\n' as i32 {
break;
}
}
}
} else if c != b'\n' as i32 && valid_chars.is_none() {
if isset(MULTIBYTE) && c >= 0 {
let mut cc: u8 = c as u8; let mut accum: Vec<u8> = vec![cc];
loop {
let incomplete = match std::str::from_utf8(&accum) {
Ok(_) => false,
Err(e) => e.error_len().is_none(),
};
if !incomplete {
break;
}
let nc = read1char(1); if nc < 0 {
break; }
cc = nc as u8; accum.push(cc);
}
let _ = cc;
}
let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"\n");
}
if let Some(saved) = gettyinfo() {
settyinfo(&saved);
}
c }
thread_local! {
static SPCK_D: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
static SPCK_BEST: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
static SPCK_GUESS: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
static SPCK_PAT: std::cell::RefCell<Option<crate::ported::pattern::Patprog>>
= const { std::cell::RefCell::new(None) };
static SPCK_NAMEPAT: std::cell::RefCell<Option<crate::ported::pattern::Patprog>>
= const { std::cell::RefCell::new(None) };
}
fn spscan(name: &str) {
let guess = SPCK_GUESS.with(|g| g.borrow().clone()).unwrap_or_default();
if guess.is_empty() {
return;
}
let thresh = guess.len() / 4 + 1; let nd = spdist(name, &guess, thresh) as i32; let d = SPCK_D.with(|c| c.get());
if nd < d {
let allow = SPCK_PAT.with(|p| {
p.borrow()
.as_ref()
.map(|prog| !crate::ported::pattern::pattry(prog, name))
.unwrap_or(true)
});
if allow {
SPCK_BEST.with(|b| *b.borrow_mut() = Some(name.to_string())); SPCK_D.with(|c| c.set(nd)); }
}
}
pub fn spckword(s: &mut String, hist: i32, cmd: i32, ask: i32) {
use crate::ported::hashtable::{
aliastab_lock, cmdnamtab_lock, fillcmdnamtable, pathchecked, reswdtab_lock, shfunctab_lock,
};
use crate::ported::params::{getsparam, paramtab};
use crate::ported::pattern::{patcompile, PAT_HEAPDUP};
use crate::ported::zsh_h::{
isset, Dash, Equals, Stringg as StringTok, Tilde, AUTOCD, HASHLISTALL,
};
let _t: Option<String>;
let mut ic: char = '\0'; let mut preflen: usize = 0; let autocd = cmd != 0 && isset(AUTOCD) && s != "." && s != "..";
if s.len() < 2 {
return;
}
let bytes = s.as_bytes();
let first = bytes[0] as char;
let histdone = crate::ported::hist::histdone.load(std::sync::atomic::Ordering::Relaxed); if (histdone & HISTFLAG_NOEXEC) != 0
|| (if cmd != 0 {
first == '%' } else {
first == '-' || first == Dash })
{
return; }
if s == "in" {
return; }
if cmd != 0 {
let known = shfunctab_lock() .read()
.map(|t| t.get(s).is_some())
.unwrap_or(false)
|| BUILTINS .iter()
.any(|b| b.node.nam == *s)
|| cmdnamtab_lock() .read()
.map(|t| t.get(s).is_some())
.unwrap_or(false)
|| aliastab_lock() .read()
.map(|t| t.get(s).is_some())
.unwrap_or(false)
|| reswdtab_lock() .read()
.map(|t| t.get(s).is_some())
.unwrap_or(false);
if known {
return; }
if isset(HASHLISTALL) {
let path: Vec<String> =
getsparam("PATH") .map(|p| p.split(':').map(String::from).collect())
.unwrap_or_default();
fillcmdnamtable(&path); if cmdnamtab_lock() .read()
.map(|t| t.get(s).is_some())
.unwrap_or(false)
{
return; }
}
}
let mut start = 0usize;
let bytes = s.as_bytes(); if !bytes.is_empty() {
let c0 = bytes[0] as char;
if c0 == Tilde || c0 == Equals || c0 == StringTok {
start = 1; }
}
{
let mut buf = s.clone().into_bytes();
let mut i = start;
let mut had_dash_only = true; while i < buf.len() {
let b = buf[i];
if itok(b) {
if b as char == Dash {
buf[i] = b'-'; } else {
return; }
} else {
had_dash_only = had_dash_only && false;
}
i += 1;
}
let _ = had_dash_only;
*s = String::from_utf8_lossy(&buf).into_owned();
}
SPCK_BEST.with(|b| *b.borrow_mut() = None);
SPCK_D.with(|c| c.set(100)); let t_pos = s.find('/').unwrap_or(s.len()); let bytes = s.as_bytes();
if !bytes.is_empty() && (bytes[0] as char) == Tilde && t_pos == bytes.len() {
return; }
if let Some(ci) = getsparam("CORRECT_IGNORE") {
let prog = patcompile(&ci, PAT_HEAPDUP, None); SPCK_PAT.with(|p| *p.borrow_mut() = prog);
} else {
SPCK_PAT.with(|p| *p.borrow_mut() = None); }
if let Some(ci) = getsparam("CORRECT_IGNORE_FILE") {
let prog = patcompile(&ci, PAT_HEAPDUP, None); SPCK_NAMEPAT.with(|p| *p.borrow_mut() = prog);
} else {
SPCK_NAMEPAT.with(|p| *p.borrow_mut() = None); }
let bytes = s.as_bytes();
let first = if bytes.is_empty() {
'\0'
} else {
bytes[0] as char
};
if first == StringTok && t_pos == bytes.len() {
let guess = s[1..].to_string();
if itype_end(&guess, crate::ported::ztype_h::INAMESPC, true) == 0 {
return; }
ic = StringTok; SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess));
SPCK_D.with(|c| c.set(100)); if let Ok(t) = paramtab().read() {
for k in t.keys() {
spscan(k);
}
}
} else if first == Equals {
if t_pos != bytes.len() {
return; }
let guess = s[1..].to_string();
let path: Vec<String> = getsparam("PATH")
.map(|p| p.split(':').map(String::from).collect())
.unwrap_or_default();
let pc = pathchecked.load(std::sync::atomic::Ordering::Relaxed);
if crate::ported::exec::hashcmd(&guess, &path[pc.min(path.len())..]).is_some() {
return; }
SPCK_D.with(|c| c.set(100)); ic = Equals; SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess));
if let Ok(t) = aliastab_lock().read() {
for (k, _) in t.iter() {
spscan(k);
}
}
if let Ok(t) = cmdnamtab_lock().read() {
for (k, _) in t.iter() {
spscan(k);
}
}
} else {
let mut guess = s.clone(); if !guess.is_empty()
&& ((guess.as_bytes()[0] as char) == Tilde
|| (guess.as_bytes()[0] as char) == StringTok)
{
ic = guess.as_bytes()[0] as char; if t_pos + 1 >= s.len() {
return; }
let saved_noerrs =
crate::ported::exec::noerrs.load(std::sync::atomic::Ordering::Relaxed);
crate::ported::exec::noerrs.store(2, std::sync::atomic::Ordering::Relaxed); guess = crate::ported::subst::singsub(&guess); crate::ported::exec::noerrs.store(saved_noerrs, std::sync::atomic::Ordering::Relaxed);
if guess.is_empty() {
return; }
let t_len = s.len() - t_pos;
preflen = guess.len().saturating_sub(t_len);
}
let cstr = match std::ffi::CString::new(unmeta(&guess).as_str()) {
Ok(c) => c,
Err(_) => return,
};
if unsafe { libc::access(cstr.as_ptr(), libc::F_OK) } == 0 {
return; }
let path_obj = std::path::Path::new(&guess);
let parent = path_obj
.parent()
.and_then(|p| p.to_str())
.filter(|s| !s.is_empty())
.unwrap_or(".");
let basename = path_obj.file_name().and_then(|n| n.to_str()).unwrap_or("");
let best = spname(basename, parent);
SPCK_BEST.with(|b| *b.borrow_mut() = best);
SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess.clone()));
if t_pos == s.len() && cmd != 0 {
let path: Vec<String> = getsparam("PATH")
.map(|p| p.split(':').map(String::from).collect())
.unwrap_or_default();
let pc = pathchecked.load(std::sync::atomic::Ordering::Relaxed);
if crate::ported::exec::hashcmd(&guess, &path[pc.min(path.len())..]).is_some() {
return; }
SPCK_D.with(|c| c.set(100)); if let Ok(t) = reswdtab_lock().read() {
for (k, _) in t.iter() {
spscan(k);
}
}
if let Ok(t) = aliastab_lock().read() {
for (k, _) in t.iter() {
spscan(k);
}
}
if let Ok(t) = shfunctab_lock().read() {
for (k, _) in t.iter() {
spscan(k);
}
}
for b in BUILTINS.iter() {
spscan(&b.node.nam);
}
if let Ok(t) = cmdnamtab_lock().read() {
for (k, _) in t.iter() {
spscan(k);
}
}
if autocd {
let unmeta_guess = crate::ported::utils::unmeta(&guess);
if crate::ported::builtin::cd_able_vars(&unmeta_guess).is_some() {
return; }
let cdpath: Vec<String> = crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| t.get("cdpath").and_then(|pm| pm.u_arr.clone()))
.unwrap_or_default();
let mut cur_d = SPCK_D.with(|c| c.get());
let mut cur_best = SPCK_BEST.with(|b| b.borrow().clone());
for pp in cdpath.iter() {
if let Some((bestcd, thisdist)) = mindist(pp, &s) {
if (thisdist as i32) < cur_d {
cur_best = Some(bestcd);
cur_d = thisdist as i32;
}
}
}
SPCK_BEST.with(|b| *b.borrow_mut() = cur_best);
SPCK_D.with(|c| c.set(cur_d));
}
}
}
if (errflag.load(std::sync::atomic::Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
return; }
let best = SPCK_BEST.with(|b| b.borrow().clone());
let guess = SPCK_GUESS.with(|g| g.borrow().clone()).unwrap_or_default();
let Some(mut best) = best else {
return;
};
if best.len() <= 1 || best == guess {
return;
}
if ic != '\0' {
if preflen > 0 {
if !best.starts_with(&guess[..preflen.min(guess.len())]) {
return; }
let t_off = s.len() - (s.len() - t_pos);
let mut u = String::with_capacity(t_off + best.len() - preflen + 1);
u.push_str(&s[..t_off]);
u.push_str(&best[preflen..]);
best = u;
} else {
best = format!("\0{}", best);
}
let pound = crate::ported::zsh_h::Pound as u8;
let zt = crate::ported::lex::ztokens.as_bytes();
let token_char = if (ic as u8) >= pound {
let idx = (ic as u8 - pound) as usize;
if idx < zt.len() {
zt[idx] as char
} else {
ic
}
} else {
ic
};
if !s.is_empty() {
let mut sb = s.clone().into_bytes();
sb[0] = token_char as u8;
*s = String::from_utf8_lossy(&sb).into_owned();
}
if !best.is_empty() {
let mut bb = best.into_bytes();
bb[0] = token_char as u8;
best = String::from_utf8_lossy(&bb).into_owned();
}
}
let x: char;
if ask != 0 {
x = 'n';
} else {
x = 'y'; }
if x == 'y' {
*s = best; if hist != 0 {
}
} else if x == 'a' {
crate::ported::hist::histdone
.fetch_or(HISTFLAG_NOEXEC, std::sync::atomic::Ordering::Relaxed); } else if x == 'e' {
crate::ported::hist::histdone.fetch_or(
HISTFLAG_NOEXEC | crate::ported::zsh_h::HISTFLAG_RECALL,
std::sync::atomic::Ordering::Relaxed,
); }
if ic != '\0' && !s.is_empty() {
let mut sb = s.clone().into_bytes();
sb[0] = ic as u8;
*s = String::from_utf8_lossy(&sb).into_owned();
}
}
pub fn ztrftimebuf(bufsizeptr: &mut i32, decr: i32) -> i32 {
if *bufsizeptr <= decr {
return 1;
}
*bufsizeptr -= decr;
0
}
pub fn ztrftime(fmt: &str, time: std::time::SystemTime) -> String {
let duration = time.duration_since(UNIX_EPOCH).unwrap_or_default();
let secs = duration.as_secs() as i64;
let nsec = duration.subsec_nanos() as u64;
#[cfg(unix)]
unsafe {
let tm = libc::localtime(&secs);
if tm.is_null() {
return String::new();
}
let tm_ref = &*tm;
let mut preprocessed = String::with_capacity(fmt.len());
let bytes = fmt.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 1 < bytes.len() {
let mut j = i + 1;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
let digs: u32 = if j > i + 1 {
std::str::from_utf8(&bytes[i + 1..j])
.unwrap_or("3")
.parse()
.unwrap_or(3)
} else {
3
};
if j < bytes.len() && bytes[j] == b'.' {
let digs = digs.min(9);
let trunc_div: u64 = 10u64.pow(9 - digs);
let val = nsec / trunc_div;
preprocessed.push_str(&format!("{:0width$}", val, width = digs as usize));
i = j + 1;
continue;
}
let next = bytes[i + 1];
match next {
b'K' => {
preprocessed.push_str(&format!("{}", tm_ref.tm_hour));
i += 2;
continue;
}
b'L' => {
let mut h12 = tm_ref.tm_hour % 12;
if h12 == 0 {
h12 = 12;
}
preprocessed.push_str(&format!("{}", h12));
i += 2;
continue;
}
b'f' => {
preprocessed.push_str(&format!("{}", tm_ref.tm_mday));
i += 2;
continue;
}
_ => {}
}
}
preprocessed.push(bytes[i] as char);
i += 1;
}
let mut buf = vec![0u8; 256];
let c_fmt = CString::new(preprocessed).unwrap_or_default();
let len = libc::strftime(
buf.as_mut_ptr() as *mut libc::c_char,
buf.len(),
c_fmt.as_ptr(),
tm,
);
if len > 0 {
buf.truncate(len);
String::from_utf8_lossy(&buf).to_string()
} else {
String::new()
}
}
#[cfg(not(unix))]
{
let _ = (fmt, secs, nsec);
String::new()
}
}
pub fn zjoin(arr: &[String], delim: char) -> String {
arr.join(&delim.to_string())
}
pub fn colonsplit(s: &str, uniq: bool) -> Vec<String> {
let ct = s.matches(':').count(); let mut ret: Vec<String> = Vec::with_capacity(ct + 2);
let bytes = s.as_bytes();
let mut t: usize = 0;
loop {
let seg_start = t; while t < bytes.len() && bytes[t] != b':' {
t += 1;
}
let seg = &s[seg_start..t];
if !uniq || !ret.iter().any(|p| p == seg) {
ret.push(seg.to_string()); }
if t >= bytes.len() {
break;
} t += 1; }
ret }
pub fn skipwsep(s: &str) -> (&str, usize) {
let bytes = s.as_bytes();
let mut i: usize = 0;
let mut count: usize = 0;
while i < bytes.len() {
let b = if bytes[i] == Meta && i + 1 < bytes.len() {
bytes[i + 1] ^ 32
} else {
bytes[i]
};
if !iwsep(b) {
break;
}
if bytes[i] == Meta {
i += 1;
}
i += 1;
count += 1;
}
(&s[i..], count)
}
pub fn spacesplit(s: &str, allownull: bool) -> Vec<String> {
if allownull {
s.split([' ', '\t', '\n']).map(|p| p.to_string()).collect()
} else {
s.split_whitespace().map(|p| p.to_string()).collect()
}
}
pub fn findsep(s: &str, sep: Option<&str>) -> Option<usize> {
match sep {
Some(sep) if sep.len() == 1 => s.find(sep.chars().next().unwrap()),
Some(sep) => s.find(sep),
None => {
s.find(|c: char| c.is_ascii_whitespace())
}
}
}
pub fn findword<'a>(s: &'a str, sep: Option<&'a str>) -> Option<(&'a str, &'a str)> {
let s = match sep {
Some(_) => s,
None => s.trim_start(),
};
if s.is_empty() {
return None;
}
match sep {
Some(sep) => {
if let Some(pos) = s.find(sep) {
Some((&s[..pos], &s[pos + sep.len()..]))
} else {
Some((s, ""))
}
}
None => {
let end = s.find(|c: char| c.is_ascii_whitespace()).unwrap_or(s.len());
Some((&s[..end], &s[end..]))
}
}
}
pub fn wordcount(s: &str, sep: Option<&str>, mul: i32) -> i32 {
let bytes = s.as_bytes();
if let Some(sep) = sep {
let sep_bytes = sep.as_bytes();
let sl = sep_bytes.len();
let mut r: i32 = 1;
let mut pos = 0;
while pos <= bytes.len() {
let rest = &bytes[pos..];
let c_offset = match sep_bytes.is_empty() {
true => Some(0usize),
false => rest.windows(sl).position(|w| w == sep_bytes),
};
let Some(c) = c_offset else { break };
let after_off = pos + c + sl;
let following_nonempty = after_off < bytes.len();
let cond_b = sl != 0 || following_nonempty;
if (c != 0 || mul != 0) && cond_b {
r += 1;
}
if sl == 0 {
pos += 1;
} else {
pos += c + sl;
}
}
r
} else {
let mut s_pos = 0usize;
let t_orig = s_pos;
let mut r: i32 = 0;
if mul <= 0 {
while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
s_pos += 1;
}
}
let has_word_now = s_pos < bytes.len() && !iwsep(bytes[s_pos]);
if has_word_now || (mul < 0 && t_orig != s_pos) {
r += 1;
}
while s_pos < bytes.len() {
let word_start = s_pos;
while s_pos < bytes.len() && !iwsep(bytes[s_pos]) {
s_pos += 1;
}
if s_pos > word_start && mul <= 0 {
while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
s_pos += 1;
}
}
if s_pos < bytes.len() && iwsep(bytes[s_pos]) {
s_pos += 1;
}
let t_after = s_pos;
if mul <= 0 {
while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
s_pos += 1;
}
}
if s_pos < bytes.len() {
r += 1;
} else {
if mul < 0 && t_after != s_pos {
r += 1;
}
break;
}
}
r
}
}
pub fn sepjoin(arr: &[String], sep: Option<&str>) -> String {
if arr.is_empty() {
return String::new();
}
let ifs_storage: String;
let sep_str: &str = match sep {
Some(s) => s, None => {
let ifs = getsparam("IFS").unwrap_or_default();
if !ifs.is_empty() && !ifs.starts_with(' ') {
ifs_storage = ifs
.chars()
.next()
.map(|c| c.to_string())
.unwrap_or_default();
&ifs_storage
} else {
" "
}
}
};
arr.join(sep_str)
}
pub fn sepsplit(s: &str, sep: Option<&str>, allownull: bool) -> Vec<String> {
let s = if s.starts_with('\x00') && s.len() > 1 {
&s[1..]
} else {
s
};
match sep {
None => spacesplit(s, allownull),
Some("") => {
if allownull {
s.chars().map(|c| c.to_string()).collect()
} else {
s.chars()
.map(|c| c.to_string())
.filter(|c| !c.is_empty())
.collect()
}
}
Some(sep) => {
let parts: Vec<String> = s.split(sep).map(|p| p.to_string()).collect();
if allownull {
parts
} else {
parts.into_iter().filter(|p| !p.is_empty()).collect()
}
}
}
}
pub fn getshfunc(nam: &str) -> Option<shfunc> {
let tab = shfunctab_lock().read().expect("shfunctab poisoned");
tab.get(nam).cloned()
}
pub fn subst_string_by_func(
func_name: &str,
arg1: Option<&str>,
orig: &str,
) -> Option<Vec<String>> {
let osc = SFCONTEXT.load(Ordering::Relaxed); let osm = STOPMSG.load(Ordering::Relaxed);
let old_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
let mut args: Vec<String> = Vec::with_capacity(3); args.push(func_name.to_string()); if let Some(a) = arg1 {
args.push(a.to_string()); }
args.push(orig.to_string()); SFCONTEXT .store(SFC_SUBST, Ordering::Relaxed);
INCOMPFUNC.store(0, Ordering::Relaxed);
let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
.read()
.ok()
.and_then(|t| t.get(func_name).cloned());
let rc = if let Some(mut shf) = shf_clone {
let name_for_body = func_name.to_string();
let body_args = args.clone();
let body_runner = move || -> i32 {
crate::ported::exec_hooks::run_function_body(&name_for_body, &body_args[1..])
.unwrap_or(0)
};
crate::ported::exec::doshfunc(&mut shf, args.clone(), true, body_runner)
} else {
callhookfunc(func_name, Some(&args), 0, std::ptr::null_mut())
};
let ret: Option<Vec<String>> = if rc != 0 {
None } else {
getaparam("reply") };
SFCONTEXT.store(osc, Ordering::Relaxed); STOPMSG.store(osm, Ordering::Relaxed); INCOMPFUNC.store(old_incompfunc, Ordering::Relaxed); ret }
pub fn subst_string_by_hook(name: &str, arg1: Option<&str>, orig: &str) -> Option<Vec<String>> {
let mut ret: Option<Vec<String>> = None;
if getshfunc(name).is_some() {
ret = subst_string_by_func(name, arg1, orig); }
if ret.is_none() {
let arrnam = format!("{}_hook", name); if let Some(arr) = getaparam(&arrnam) {
for f in arr.iter() {
if f.is_empty() {
continue;
}
if getshfunc(f).is_some() {
ret = subst_string_by_func(f, arg1, orig); if ret.is_some() {
break; }
}
}
}
}
ret }
pub fn mkarray(s: Option<&str>) -> Vec<String> {
match s {
Some(val) => vec![val.to_string()],
None => Vec::new(),
}
}
pub fn hmkarray(s: &str) -> Vec<String> {
if s.is_empty() {
Vec::new()
} else {
vec![s.to_string()]
}
}
pub fn zbeep() {
queue_signals(); if let Ok(zbeep) = std::env::var("ZBEEP") {
let (decoded, _) = getkeystring(&zbeep); #[cfg(unix)]
{
let shtty = SHTTY.load(Ordering::Relaxed);
if shtty != -1 {
let _ = write_loop(shtty, decoded.as_bytes()); } else {
eprint!("{}", decoded);
}
}
#[cfg(not(unix))]
eprint!("{}", decoded);
} else if isset(BEEP) {
#[cfg(unix)]
{
let shtty = SHTTY.load(Ordering::Relaxed);
if shtty != -1 {
let _ = write_loop(shtty, b"\x07"); } else {
eprint!("\x07");
}
}
#[cfg(not(unix))]
eprint!("\x07");
}
unqueue_signals(); }
pub fn freearray(s: Vec<String>) {
let _ = &s; }
pub fn equalsplit(s: &str) -> Option<(String, String)> {
let eq = s.find('=')?;
Some((s[..eq].to_string(), s[eq + 1..].to_string()))
}
pub fn inittyptab() {
{
let mut flags = TYPTAB_FLAGS.lock().unwrap();
if (*flags & ZTF_INIT) == 0 {
*flags = ZTF_INIT;
}
}
let mut t = TYPTAB.lock().unwrap();
for slot in t.iter_mut() {
*slot = 0;
}
for c in 0..32u32 {
t[c as usize] = ICNTRL as u32;
t[(c + 128) as usize] = ICNTRL as u32;
}
t[127] = ICNTRL as u32;
for c in (b'0' as usize)..=(b'9' as usize) {
t[c] = (IDIGIT | IALNUM | IWORD | IIDENT | IUSER) as u32;
}
for c in (b'a' as usize)..=(b'z' as usize) {
let upper = c - (b'a' as usize) + (b'A' as usize);
let bits = (IALPHA | IALNUM | IIDENT | IUSER | IWORD) as u32;
t[c] = bits;
t[upper] = bits;
}
t[b'_' as usize] = (IIDENT | IUSER) as u32;
t[b'-' as usize] = IUSER as u32;
t[b'.' as usize] = IUSER as u32;
t[crate::ported::zsh_h::Dash as usize] = IUSER as u32;
t[b' ' as usize] |= (IBLANK | INBLANK) as u32;
t[b'\t' as usize] |= (IBLANK | INBLANK) as u32;
t[b'\n' as usize] |= INBLANK as u32;
t[0] |= IMETA as u32; {
t[Meta as usize] |= IMETA as u32;
t[Marker as usize] |= IMETA as u32;
}
{
let lo = Pound as usize;
let hi = LAST_NORMAL_TOK as usize;
for t0 in lo..=hi {
t[t0] |= (ITOK | IMETA) as u32;
}
}
{
let lo = Snull as usize;
let hi = Nularg as usize;
for t0 in lo..=hi {
t[t0] |= (ITOK | IMETA | INULL) as u32;
}
}
{
let ifs = crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)))
.filter(|s| !s.is_empty())
.unwrap_or_else(|| {
crate::ported::params::ifs_lock()
.lock()
.map(|g| g.clone())
.unwrap_or_default()
});
let src: String = if ifs.is_empty() {
DEFAULT_IFS.to_string()
} else {
ifs
};
let bytes = src.as_bytes();
let mut i = 0;
while i < bytes.len() {
let c = if bytes[i] == Meta && i + 1 < bytes.len() {
i += 1;
bytes[i] ^ 32
} else {
bytes[i]
};
if c >= 0x80 {
i += 1;
continue;
}
let cu = c as usize;
let is_inblank = (t[cu] & (INBLANK as u32)) != 0;
if is_inblank {
if i + 1 < bytes.len() && bytes[i + 1] == c {
i += 1; } else {
t[cu] |= IWSEP as u32; }
}
t[cu] |= ISEP as u32;
i += 1;
}
}
{
let wc = crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| {
t.get("WORDCHARS")
.map(|pm| crate::ported::params::wordcharsgetfn(pm))
})
.filter(|s| !s.is_empty())
.unwrap_or_else(|| {
crate::ported::params::wordchars_lock()
.lock()
.map(|g| g.clone())
.unwrap_or_default()
});
let src: String = if wc.is_empty() {
DEFAULT_WORDCHARS.to_string()
} else {
wc
};
let bytes = src.as_bytes();
let mut i = 0;
while i < bytes.len() {
let c = if bytes[i] == Meta && i + 1 < bytes.len() {
i += 1;
bytes[i] ^ 32
} else {
bytes[i]
};
if c < 0x80 {
t[c as usize] |= IWORD as u32; }
i += 1;
}
}
{
for &b in SPECCHARS.as_bytes() {
t[b as usize] |= ISPECIAL as u32; }
}
{
let flags = *TYPTAB_FLAGS.lock().unwrap();
if (flags & ZTF_SP_COMMA) != 0 {
t[b',' as usize] |= ISPECIAL as u32; }
}
{
let bangchar2 = bangchar.load(Ordering::SeqCst) as usize;
let flags = *TYPTAB_FLAGS.lock().unwrap();
let interact_flag = (flags & ZTF_INTERACT) != 0;
let banghist = isset(BANGHIST);
if banghist && bangchar2 != 0 && bangchar2 < 256 && interact_flag {
*TYPTAB_FLAGS.lock().unwrap() |= ZTF_BANGCHAR; t[bangchar2] |= ISPECIAL as u32; } else {
*TYPTAB_FLAGS.lock().unwrap() &= !ZTF_BANGCHAR; }
}
{
for &b in PATCHARS.as_bytes() {
t[b as usize] |= IPATTERN as u32; }
}
}
pub fn makecommaspecial(yesno: bool) {
let mut flags = TYPTAB_FLAGS.lock().unwrap();
let mut tab = TYPTAB.lock().unwrap();
if yesno {
*flags |= ZTF_SP_COMMA; tab[b',' as usize] |= ISPECIAL as u32; } else {
*flags &= !ZTF_SP_COMMA; tab[b',' as usize] &= !(ISPECIAL as u32); }
}
pub fn makebangspecial(yesno: bool) {
let bc = bangchar.load(Ordering::SeqCst) as usize;
if bc == 0 || bc >= 256 {
return;
}
let flags = *TYPTAB_FLAGS.lock().unwrap();
let mut tab = TYPTAB.lock().unwrap();
if !yesno {
tab[bc] &= !(ISPECIAL as u32); } else if (flags & ZTF_BANGCHAR) != 0 {
tab[bc] |= ISPECIAL as u32; }
}
pub fn wcsiblank(wc: char) -> bool {
wc.is_whitespace() && wc != '\n'
}
pub fn wcsitype(c: char, itype: u32) -> bool {
if !isset(MULTIBYTE) {
if (c as u32) < 256 {
let tab = TYPTAB.lock().unwrap();
return (tab[c as usize] & itype) != 0;
}
return false;
}
if (c as u32) < 128 {
let tab = TYPTAB.lock().unwrap();
return (tab[c as usize] & itype) != 0;
}
let cls = itype as u16;
if cls == IIDENT {
if isset(POSIXIDENTIFIERS) {
return false; }
return c.is_alphanumeric(); }
if cls == IWORD {
if c.is_alphanumeric() {
return true;
} if unicode_width::UnicodeWidthChar::width(c).unwrap_or(1) == 0 {
return true;
}
let w = crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| {
t.get("WORDCHARS")
.map(|pm| crate::ported::params::wordcharsgetfn(pm))
})
.unwrap_or_default();
return w.chars().any(|x| x == c);
}
if cls == ISEP {
let ifs = crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)))
.unwrap_or_default();
return ifs.chars().any(|x| x == c);
}
let _ = IALNUM;
c.is_alphanumeric() }
pub fn itype_end(s: &str, mut itype: u32, once: bool) -> usize {
use crate::ported::zsh_h::{isset, Meta, EMULATE_KSH, EMULATION, MULTIBYTE, POSIXIDENTIFIERS};
use crate::ported::ztype_h::{itok, zistype, IIDENT, INAMESPC};
let mut start = 0usize;
if itype == INAMESPC {
itype = IIDENT as u32;
if !isset(POSIXIDENTIFIERS) || EMULATION(EMULATE_KSH) {
let first_is_dot = s.as_bytes().first().copied() == Some(b'.');
let recurse_start = if first_is_dot { 1 } else { 0 };
let t = recurse_start + itype_end(&s[recurse_start..], itype, false);
if t > recurse_start {
let t_byte = s.as_bytes().get(t).copied();
if t_byte == Some(b'.') {
start = t + 1; } else if !once {
return t; }
}
}
}
let bytes = s.as_bytes();
let mb = isset(MULTIBYTE) && (itype != IIDENT as u32 || !isset(POSIXIDENTIFIERS));
if mb {
let mut i = start;
while i < bytes.len() {
let b = bytes[i];
if itok(b) {
if !zistype(b, itype) {
break;
}
i += 1;
} else if b == Meta {
let nxt = bytes.get(i + 1).copied().unwrap_or(0) ^ 0x20;
if (nxt as u32) > 127 || !zistype(nxt, itype) {
break;
}
i += 2;
} else if b < 0x80 {
if !zistype(b, itype) {
break;
}
i += 1;
} else {
let tail = match std::str::from_utf8(&bytes[i..]) {
Ok(t) => t,
Err(_) => break, };
let ch = match tail.chars().next() {
Some(c) => c,
None => break,
};
if !wcsitype(ch, itype) {
break;
}
i += ch.len_utf8();
}
if once {
break;
}
}
i
} else {
let mut i = start;
while i < bytes.len() {
let b = bytes[i];
let chr = if b == Meta {
bytes.get(i + 1).copied().unwrap_or(0) ^ 0x20
} else {
b
};
if !zistype(chr, itype) {
break;
}
i += if b == Meta { 2 } else { 1 };
if once {
break;
}
}
i
}
}
pub fn arrdup(s: &[String]) -> Vec<String> {
s.to_vec()
}
pub fn arrdup_max(s: &[String], max: usize) -> Vec<String> {
s.iter().take(max).cloned().collect()
}
pub fn zarrdup(s: &[String]) -> Vec<String> {
s.to_vec()
}
pub fn wcs_zarrdup(s: &[String]) -> Vec<String> {
s.to_vec()
}
pub fn spname(name: &str, dir: &str) -> Option<String> {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return None,
};
let mut best = None;
let mut best_dist = 4;
for entry in entries.flatten() {
if let Some(entry_name) = entry.file_name().to_str() {
let dist = spdist(name, entry_name, best_dist);
if dist < best_dist {
best_dist = dist;
best = Some(entry_name.to_string());
}
}
}
best
}
pub fn mindist(dir: &str, name: &str) -> Option<(String, usize)> {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return None,
};
let mut best = None;
let mut best_dist = 4;
for entry in entries.flatten() {
if let Some(entry_name) = entry.file_name().to_str() {
let dist = spdist(name, entry_name, best_dist);
if dist < best_dist {
best_dist = dist;
best = Some(entry_name.to_string());
}
}
}
best.map(|name| (name, best_dist))
}
pub fn spdist(s: &str, t: &str, thresh: usize) -> usize {
const QWERTYKEYMAP: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
\t1234567890-=\t\
\tqwertyuiop[]\t\
\tasdfghjkl;'\n\t\
\tzxcvbnm,./\t\t\t\
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
\t!@#$%^&*()_+\t\
\tQWERTYUIOP{}\t\
\tASDFGHJKL:\"\n\t\
\tZXCVBNM<>?\n\n\t\
\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
const DVORAKKEYMAP: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
\t1234567890[]\t\
\t',.pyfgcrl/=\t\
\taoeuidhtns-\n\t\
\t;qjkxbmwvz\t\t\t\
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
\t!@#$%^&*(){}\t\
\t\"<>PYFGCRL?+\t\
\tAOEUIDHTNS_\n\t\
\t:QJKXBMWVZ\n\n\t\
\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
let keymap = if isset(DVORAK) {
DVORAKKEYMAP.as_bytes()
} else {
QWERTYKEYMAP.as_bytes()
};
let s_b = s.as_bytes();
let t_b = t.as_bytes();
if s == t {
return 0;
}
let mut p = 0usize;
let mut q = 0usize;
while p < s_b.len() && q < t_b.len() && tulower(s_b[p] as char) == tulower(t_b[q] as char) {
p += 1;
q += 1;
}
if p == s_b.len() && q == t_b.len() {
return 1;
}
if thresh == 0 {
return 200;
}
p = 0;
q = 0;
while p < s_b.len() && q < t_b.len() {
if s_b[p] == t_b[q] {
p += 1;
q += 1;
continue;
}
if p + 1 < s_b.len() && q + 1 < t_b.len() && s_b[p + 1] == t_b[q] && t_b[q + 1] == s_b[p] {
let s_tail = std::str::from_utf8(&s_b[p + 2..]).unwrap_or("");
let t_tail = std::str::from_utf8(&t_b[q + 2..]).unwrap_or("");
return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 1;
}
if p + 1 < s_b.len() && s_b[p + 1] == t_b[q] {
let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
let t_tail = std::str::from_utf8(&t_b[q..]).unwrap_or("");
return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
}
if q + 1 < t_b.len() && s_b[p] == t_b[q + 1] {
let s_tail = std::str::from_utf8(&s_b[p..]).unwrap_or("");
let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
}
break;
}
if (p == s_b.len() && (t_b.len() - q) == 1) || (q == t_b.len() && (s_b.len() - p) == 1) {
return 2;
}
p = 0;
q = 0;
while p < s_b.len() && q < t_b.len() {
if p + 1 < s_b.len() && q + 1 < t_b.len() && s_b[p] != t_b[q] && s_b[p + 1] == t_b[q + 1] {
let pos = keymap.iter().position(|&b| b == s_b[p]);
let z_ok = match pos {
Some(i) => keymap[i] != b'\n' && keymap[i] != b'\t',
None => false,
};
if !z_ok {
let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 1;
}
let t0 = pos.unwrap() as isize;
let offsets: [isize; 8] = [-15, -14, -13, -1, 1, 13, 14, 15];
let adjacent = offsets.iter().any(|&off| {
let idx = t0 + off;
if idx >= 0 && (idx as usize) < keymap.len() {
keymap[idx as usize] == t_b[q]
} else {
false
}
});
if adjacent {
let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
}
return 200;
} else if p < s_b.len() && q < t_b.len() && s_b[p] != t_b[q] {
break;
}
p += 1;
q += 1;
}
200
}
#[cfg(unix)]
pub fn setcbreak() -> bool {
if let Some(mut ti) = gettyinfo() {
ti.c_lflag &= !(libc::ICANON | libc::ECHO);
ti.c_cc[libc::VMIN] = 1;
ti.c_cc[libc::VTIME] = 0;
settyinfo(&ti)
} else {
false
}
}
#[cfg(not(unix))]
pub fn setcbreak() -> bool {
false
}
#[cfg(unix)]
pub fn attachtty(pgrp: i32) {
if !(jobbing() && interact()) {
return; }
let shtty = SHTTY.load(Ordering::Relaxed); if shtty == -1 {
return;
}
let ep = ATTACHTTY_EP.load(Ordering::Relaxed);
let rc = unsafe { libc::tcsetpgrp(shtty, pgrp) }; if rc == -1 && ep == 0 {
let mypgrp_val = *crate::ported::jobs::MYPGRP .get_or_init(|| Mutex::new(0))
.lock()
.unwrap();
if pgrp != mypgrp_val && unsafe { libc::kill(-pgrp, 0) } == -1 {
attachtty(mypgrp_val); } else {
let errno_val = io::Error::last_os_error().raw_os_error().unwrap_or(0);
if errno_val != libc::ENOTTY {
zwarn(&format!(
"can't set tty pgrp: {}", io::Error::from_raw_os_error(errno_val)
));
let _ = io::stderr().flush(); }
opt_state_set("monitor", false); ATTACHTTY_EP.store(1, Ordering::Relaxed); }
} else if rc != -1 {
*crate::ported::jobs::LAST_ATTACHED_PGRP .get_or_init(|| Mutex::new(0))
.lock()
.unwrap() = pgrp;
}
}
#[cfg(unix)]
pub fn gettygrp() -> i32 {
let shtty = SHTTY.load(Ordering::Relaxed);
if shtty == -1 {
return -1; }
unsafe { libc::tcgetpgrp(shtty) } }
pub fn metafy(buf: &str) -> String {
let bytes = buf.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
for &b in bytes {
if imeta_byte(b) {
out.push(Meta);
out.push(b ^ 32);
} else {
out.push(b);
}
}
String::from_utf8(out.clone()).unwrap_or_else(|_| String::from_utf8_lossy(&out).into_owned())
}
pub fn ztrdup_metafy(s: &str) -> String {
metafy(s)
}
pub fn unmetafy(s: &mut Vec<u8>) -> usize {
let mut p: usize = 0;
while p < s.len() && s[p] != Meta {
p += 1;
}
let mut t: usize = p;
while p < s.len() {
let b = s[p];
s[t] = b;
p += 1;
if b == Meta && p < s.len() {
s[t] = s[p] ^ 32;
p += 1;
}
t += 1;
}
s.truncate(t);
t
}
pub fn metalen(s: &str, len: usize) -> usize {
let bytes = s.as_bytes();
let mut mlen = len; let mut remaining = len;
let mut i = 0;
while remaining > 0 && i < bytes.len() {
if bytes[i] == Meta {
mlen += 1; i += 2; } else {
i += 1;
}
remaining -= 1;
}
mlen
}
pub fn unmeta(s: &str) -> String {
let bytes = s.as_bytes();
if !bytes.iter().any(|&b| b == Meta) {
return s.to_string();
}
let mut buf = bytes.to_vec();
let len = unmetafy(&mut buf); buf.truncate(len);
String::from_utf8_lossy(&buf).into_owned()
}
pub fn unmeta_one(s: &str) -> (char, usize) {
let bytes = s.as_bytes();
if bytes.is_empty() {
return ('\0', 0);
}
if bytes[0] == Meta && bytes.len() > 1 {
((bytes[1] ^ 32) as char, 2)
} else {
(bytes[0] as char, 1)
}
}
pub fn ztrcmp(s1: &str, s2: &str) -> std::cmp::Ordering {
let b1 = s1.as_bytes();
let b2 = s2.as_bytes();
let mut i1 = 0;
let mut i2 = 0;
while i1 < b1.len() && i2 < b2.len() && b1[i1] == b2[i2] {
i1 += 1;
i2 += 1;
}
let c1: i32 = if i1 >= b1.len() {
-1
} else if b1[i1] == Meta && i1 + 1 < b1.len() {
(b1[i1 + 1] ^ 32) as i32
} else {
b1[i1] as i32
};
let c2: i32 = if i2 >= b2.len() {
-1
} else if b2[i2] == Meta && i2 + 1 < b2.len() {
(b2[i2 + 1] ^ 32) as i32
} else {
b2[i2] as i32
};
c1.cmp(&c2)
}
pub fn ztrlen(s: &str) -> usize {
let mut len = 0;
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
len += 1;
if chars[i] as u32 == Meta as u32 && i + 1 < chars.len() {
i += 2;
} else {
i += 1;
}
}
len
}
pub fn ztrlenend(s: &str, eptr: usize) -> usize {
let bytes = s.as_bytes();
let cap = eptr.min(bytes.len());
let mut l = 0;
let mut i = 0;
while i < cap {
if bytes[i] == Meta {
i += 2;
} else {
i += 1;
}
l += 1;
}
l
}
pub fn ztrsub(buf: &str, start: usize, end: usize) -> usize {
let bytes = buf.as_bytes();
let end = end.min(bytes.len());
let start = start.min(end);
let mut l = (end - start) as isize; let mut i = start;
while i < end {
if bytes[i] == Meta {
i += 2; l -= 1; } else {
i += 1;
}
}
l.max(0) as usize
}
pub fn zreaddir(dir: &mut fs::ReadDir, ignoredots: i32) -> Option<String> {
for entry in dir.by_ref() {
let Ok(e) = entry else { continue };
let Ok(name) = e.file_name().into_string() else {
continue;
};
if ignoredots != 0 && (name == "." || name == "..") {
continue; }
return Some(name); }
None }
pub fn zputs(s: &str, stream: &mut dyn std::io::Write) -> i32 {
let bytes = s.as_bytes(); let mut i = 0;
while i < bytes.len() {
let c: u8; if bytes[i] == Meta {
if i + 1 < bytes.len() {
c = bytes[i + 1] ^ 32;
i += 1; } else {
i += 1;
continue;
}
} else if itok(bytes[i]) {
i += 1;
continue;
} else {
c = bytes[i]; }
i += 1; if stream.write_all(&[c]).is_err() {
return -1; }
}
0 }
pub fn nicedup(s: &str, heap: i32) -> String {
let retstr: Option<String>;
let mut slot: Option<String> = None;
let _ = mb_niceformat(
s,
None,
Some(&mut slot),
if heap != 0 { NICEFLAG_HEAP } else { 0 },
);
retstr = slot;
retstr.unwrap_or_default()
}
pub fn nicedupstring(s: &str) -> String {
nicedup(s, 1) }
pub fn nicezputs(s: &str, stream: &mut dyn std::io::Write) -> i32 {
let _ = mb_niceformat(s, Some(stream), None, 0);
0 }
pub fn niceztrlen(s: &str) -> usize {
sb_niceformat(s, None, None, 0)
}
pub fn mb_niceformat(
s: &str,
mut stream: Option<&mut dyn std::io::Write>,
outstrp: Option<&mut Option<String>>,
flags: i32,
) -> usize {
let mut l: usize = 0; let mut newl: usize; let mut umlen: usize; let mut eol: bool = false; let mut ums: Vec<u8>; let mut ptr: usize; let mut fmt: String; let mut outstr: Option<String>;
if outstrp.is_some() {
outstr = Some(String::with_capacity(5 * s.len())); } else {
outstr = None; }
let detok = untokenize(s);
let unmeta_str = unmeta(&detok);
ums = unmeta_str.into_bytes();
umlen = ums.len(); ptr = 0;
while umlen > 0 {
let cnt: usize;
let decoded_c: Option<char>;
if eol {
decoded_c = None;
cnt = 1;
} else {
let remaining = &ums[ptr..ptr + umlen];
match std::str::from_utf8(remaining) {
Ok(s_slice) => {
if let Some(ch) = s_slice.chars().next() {
decoded_c = Some(ch);
cnt = ch.len_utf8();
} else {
decoded_c = None;
cnt = 1;
}
}
Err(e) => {
let valid_up_to = e.valid_up_to();
if valid_up_to > 0 {
let valid_slice =
unsafe { std::str::from_utf8_unchecked(&remaining[..valid_up_to]) };
let ch = valid_slice.chars().next().unwrap();
decoded_c = Some(ch);
cnt = ch.len_utf8();
} else if e.error_len().is_none() {
eol = true;
decoded_c = None; cnt = 1;
} else {
decoded_c = None;
cnt = 1;
}
}
}
}
let cnt_used: usize;
match decoded_c {
None => {
fmt = nicechar_sel(ums[ptr] as char, (flags & NICEFLAG_QUOTE) != 0);
newl = fmt.len(); cnt_used = 1; }
Some(c) if c == '\0' => {
cnt_used = 1; if c == '\'' && (flags & NICEFLAG_QUOTE) != 0 {
fmt = "\\'".to_string(); newl = 2; } else if c == '\\' && (flags & NICEFLAG_QUOTE) != 0 {
fmt = "\\\\".to_string(); newl = 2; } else {
let mut width: usize = 0;
fmt =
wcs_nicechar_sel(c, Some(&mut width), None, (flags & NICEFLAG_QUOTE) != 0);
newl = width;
}
}
Some(c) => {
cnt_used = cnt;
if c == '\'' && (flags & NICEFLAG_QUOTE) != 0 {
fmt = "\\'".to_string(); newl = 2; } else if c == '\\' && (flags & NICEFLAG_QUOTE) != 0 {
fmt = "\\\\".to_string(); newl = 2; } else {
let mut width: usize = 0;
fmt =
wcs_nicechar_sel(c, Some(&mut width), None, (flags & NICEFLAG_QUOTE) != 0);
newl = width;
}
}
}
umlen -= cnt_used; ptr += cnt_used; l += newl;
if let Some(ref mut w) = stream {
let _ = w.write_all(fmt.as_bytes());
}
if let Some(ref mut buf) = outstr {
buf.push_str(&fmt); }
let _ = fmt;
}
drop(ums);
if let Some(slot) = outstrp {
*slot = outstr.take();
}
l }
pub fn is_mb_niceformat(s: &str) -> i32 {
let umlen: usize; let mut ret: i32 = 0; let mut ums: Vec<u8>; let mut ptr: usize;
let detok = untokenize(s);
let unmeta_str = unmeta(&detok);
ums = unmeta_str.into_bytes();
umlen = ums.len(); ptr = 0;
while ret == 0 && ptr < ums.len() {
let remaining = &ums[ptr..];
match std::str::from_utf8(remaining) {
Ok(s_slice) => {
for ch in s_slice.chars() {
if is_wcs_nicechar(ch) {
ret = 1; break;
}
}
break;
}
Err(e) => {
let valid_up_to = e.valid_up_to();
if valid_up_to > 0 {
let valid = unsafe { std::str::from_utf8_unchecked(&remaining[..valid_up_to]) };
for ch in valid.chars() {
if is_wcs_nicechar(ch) {
ret = 1; break;
}
}
if ret != 0 {
break;
}
ptr += valid_up_to;
continue;
}
if is_nicechar(remaining[0] as char) {
ret = 1; break;
}
ptr += 1;
}
}
}
drop(ums); ret }
pub fn mb_metacharlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
if let Some(c) = s[pos..].chars().next() {
(c.len_utf8(), Some(c))
} else {
(0, None)
}
}
pub fn mb_metacharlenconv(s: &str) -> (usize, Option<char>) {
let bytes = s.as_bytes();
if bytes.is_empty() {
return (0, None);
}
if bytes[0] == 0x83 && bytes.len() >= 2 {
let raw = bytes[1] as u32 ^ 32;
return (2, char::from_u32(raw));
}
if bytes[0] <= 0x7f {
return (1, Some(bytes[0] as char));
}
if let Some(c) = s.chars().next() {
return (c.len_utf8(), Some(c));
}
(1, None)
}
pub fn mb_metastrlenend(ptr: &str, width: bool, eptr: usize) -> usize {
if width {
ptr[..eptr.min(ptr.len())]
.chars()
.map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(1))
.sum()
} else {
ptr[..eptr.min(ptr.len())].chars().count()
}
}
pub fn mb_charlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
mb_metacharlenconv_r(s, pos)
}
pub fn mb_charlenconv(s: &str, pos: usize) -> usize {
s[pos..].chars().next().map(|c| c.len_utf8()).unwrap_or(0)
}
pub fn metacharlenconv(s: &str) -> (usize, Option<char>) {
let bytes = s.as_bytes();
if bytes.is_empty() {
return (0, None);
}
if bytes[0] == Meta && bytes.len() >= 2 {
let raw = bytes[1] as u32 ^ 32; return (2, char::from_u32(raw)); }
(1, Some(bytes[0] as char)) }
pub fn charlenconv(s: &str, len: usize) -> (usize, Option<char>) {
if len == 0 {
return (0, None);
}
let bytes = s.as_bytes();
if bytes.is_empty() {
return (0, None);
}
(1, Some(bytes[0] as char))
}
pub fn sb_niceformat(
s: &str,
mut stream: Option<&mut dyn std::io::Write>,
outstrp: Option<&mut Option<String>>,
flags: i32,
) -> usize {
let mut l: usize = 0; let mut newl: usize; let umlen: usize; let mut ums: Vec<u8>; let mut ptr: usize; let eptr: usize; let mut fmt: String; let mut outstr: Option<String>;
if outstrp.is_some() {
outstr = Some(String::with_capacity(2 * s.len())); } else {
outstr = None; }
ums = s.as_bytes().to_vec();
let ztokens_table = b"#$^*(())$=|{}[]`<>>?~`,-!'\"\\\\"; let mut detok: Vec<u8> = Vec::with_capacity(ums.len());
for &c in &ums {
if (0x84u8..=0xa1u8).contains(&c) {
if c != 0xa1u8 {
let idx = (c - 0x84) as usize;
if idx < ztokens_table.len() {
detok.push(ztokens_table[idx]);
}
}
} else {
detok.push(c); }
}
ums = detok;
umlen = unmetafy(&mut ums);
ums.truncate(umlen);
eptr = umlen; ptr = 0;
while ptr < eptr {
let c: i32 = ums[ptr] as i32; if c == b'\'' as i32 && (flags & NICEFLAG_QUOTE) != 0 {
fmt = "\\'".to_string(); newl = 2; } else if c == b'\\' as i32 && (flags & NICEFLAG_QUOTE) != 0 {
fmt = "\\\\".to_string(); newl = 2; } else {
fmt = nicechar_sel(c as u8 as char, (flags & NICEFLAG_QUOTE) != 0); newl = 1; }
ptr += 1; l += newl;
if let Some(ref mut w) = stream {
let _ = w.write_all(fmt.as_bytes());
}
if let Some(ref mut buf) = outstr {
buf.push_str(&fmt); }
let _ = fmt;
}
drop(ums);
if let Some(slot) = outstrp {
*slot = outstr.take();
}
l }
pub fn is_sb_niceformat(s: &str) -> i32 {
let umlen: usize; let mut ret: i32 = 0; let mut ums: Vec<u8>; let mut ptr: usize; let eptr: usize;
ums = s.as_bytes().to_vec(); let ztokens_table = b"#$^*(())$=|{}[]`<>>?~`,-!'\"\\\\"; let mut detok: Vec<u8> = Vec::with_capacity(ums.len());
for &c in &ums {
if (0x84u8..=0xa1u8).contains(&c) {
if c != 0xa1u8 {
let idx = (c - 0x84) as usize;
if idx < ztokens_table.len() {
detok.push(ztokens_table[idx]);
}
}
} else {
detok.push(c); }
}
ums = detok;
umlen = unmetafy(&mut ums); ums.truncate(umlen);
eptr = umlen; ptr = 0;
while ptr < eptr {
if is_nicechar(ums[ptr] as char) {
ret = 1; break; }
ptr += 1; }
drop(ums); ret }
pub(crate) fn zexpandtabs(
s: &str,
width: i32,
startpos: i32,
all_tabs: bool,
out: &mut String,
) -> i32 {
let mut startpos = startpos;
let mut at_start = true;
for c in s.chars() {
if c == '\t' {
if all_tabs || at_start {
if width <= 0 || startpos % width == 0 {
out.push(' ');
startpos += 1;
}
if width > 0 {
while startpos % width != 0 {
out.push(' ');
startpos += 1;
}
}
} else {
let rem = startpos % width;
startpos += width - rem;
out.push('\t');
}
continue;
} else if c == '\n' || c == '\r' {
out.push(c);
startpos = 0;
at_start = true;
continue;
}
at_start = false;
out.push(c);
startpos += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as i32;
}
startpos
}
pub fn hasspecial(s: &str) -> bool {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
let c = if bytes[i] == Meta && i + 1 < bytes.len() {
let v = bytes[i + 1] ^ 32;
i += 2;
v
} else {
let v = bytes[i];
i += 1;
v
};
if crate::ported::ztype_h::ispecial(c) {
return true;
}
}
false
}
pub fn addunprintable(c: char) -> String {
let b = c as u32 & 0xff;
match b as u8 {
0x00 => "\\0".to_string(),
0x07 => "\\a".to_string(), 0x08 => "\\b".to_string(), 0x0c => "\\f".to_string(), 0x0a => "\\n".to_string(), 0x0d => "\\r".to_string(), 0x09 => "\\t".to_string(), 0x0b => "\\v".to_string(), _ => format!("\\{:o}{:o}{:o}", (b >> 6) & 7, (b >> 3) & 7, b & 7),
}
}
pub fn quotestring(s: &str, quote_type: i32) -> String {
if s.is_empty() {
return if quote_type == QT_NONE {
String::new()
} else if quote_type == QT_BACKSLASH || quote_type == QT_BACKSLASH_SHOWNULL {
"''".to_string()
} else if quote_type == QT_SINGLE || quote_type == QT_SINGLE_OPTIONAL {
"''".to_string()
} else if quote_type == QT_DOUBLE {
"\"\"".to_string()
} else if quote_type == QT_DOLLARS {
"$''".to_string()
} else {
String::new()
};
}
if quote_type == QT_NONE {
s.to_string()
} else if quote_type == QT_BACKSLASH_PATTERN {
let mut result = String::with_capacity(s.len() * 2);
for c in s.chars() {
if matches!(
c,
'*' | '?' | '[' | ']' | '<' | '>' | '(' | ')' | '|' | '#' | '^' | '~'
) {
result.push('\\');
}
result.push(c);
}
result
} else if quote_type == QT_BACKSLASH || quote_type == QT_BACKSLASH_SHOWNULL {
let mut result = String::with_capacity(s.len() * 2);
let mut prev: char = '\0'; for (i, c) in s.chars().enumerate() {
let eq_tilde_gate = if c == '=' || c == '~' {
i == 0 || (isset(MAGICEQUALSUBST) && (prev == '=' || prev == ':')) || (c == '~' && isset(EXTENDEDGLOB)) } else {
true };
if c == '\n' {
result.push_str("$'\\n'");
} else if ispecial(c) && eq_tilde_gate && c.is_ascii() && !c.is_ascii_control() {
result.push('\\');
result.push(c);
} else if c.is_ascii_control() {
result.push_str("$'");
match c {
'\t' => result.push_str("\\t"),
'\r' => result.push_str("\\r"),
'\x07' => result.push_str("\\a"),
'\x08' => result.push_str("\\b"),
'\x0c' => result.push_str("\\f"),
'\x0b' => result.push_str("\\v"),
'\x1b' => result.push_str("\\e"),
c => result.push_str(&format!("\\{:03o}", c as u8)),
}
result.push('\'');
} else {
result.push(c);
}
prev = c;
}
result
} else if quote_type == QT_SINGLE {
let mut result = String::with_capacity(s.len() + 4);
result.push('\'');
for c in s.chars() {
if c == '\'' {
result.push_str("'\\''");
} else {
result.push(c);
}
}
result.push('\'');
result
} else if quote_type == QT_SINGLE_OPTIONAL {
let needs_quoting = s.chars().any(ispecial);
if !needs_quoting {
return s.to_string();
}
let mut result: Vec<char> = Vec::with_capacity(s.len() + 4);
let mut quotestart: usize = 0; let mut quotesub: u8 = 1; for c in s.chars() {
if c == '\'' {
if quotesub == 2 {
result.push('\'');
result.push('\\');
result.push('\'');
quotesub = 1;
quotestart = result.len();
} else {
result.push('\\');
result.push('\'');
quotestart = result.len();
}
} else if ispecial(c) {
if quotesub == 1 {
result.insert(quotestart, '\'');
quotesub = 2;
}
result.push(c);
} else {
result.push(c);
}
}
if quotesub == 2 {
result.push('\'');
}
result.into_iter().collect()
} else if quote_type == QT_DOUBLE {
let mut result = String::with_capacity(s.len() + 4);
result.push('"');
for c in s.chars() {
if matches!(c, '$' | '`' | '"' | '\\') {
result.push('\\');
}
result.push(c);
}
result.push('"');
result
} else if quote_type == QT_DOLLARS {
let mut result = String::with_capacity(s.len() + 4);
result.push_str("$'");
for c in s.chars() {
match c {
'\\' | '\'' => {
result.push('\\');
result.push(c);
}
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
'\x1b' => result.push_str("\\e"),
'\x07' => result.push_str("\\a"),
'\x08' => result.push_str("\\b"),
'\x0c' => result.push_str("\\f"),
'\x0b' => result.push_str("\\v"),
c if c.is_ascii_control() => {
result.push_str(&format!("\\{:03o}", c as u8));
}
c => result.push(c),
}
}
result.push('\'');
result
} else if quote_type == QT_BACKTICK {
s.replace('`', "\\`")
} else {
s.to_string()
}
}
pub(crate) fn quotedzputs(s: &str) -> String {
if s.is_empty() {
return "''".to_string(); }
if is_mb_niceformat(s) != 0 {
let mut substr: Option<String> = None;
let _ = mb_niceformat(s, None, Some(&mut substr), NICEFLAG_QUOTE | NICEFLAG_NODUP);
return format!("$'{}'", substr.unwrap_or_default()); }
if !hasspecial(s) {
return s.to_string(); }
let mut out = String::with_capacity(s.len() + 2);
let bytes = s.as_bytes();
let csh_junkie = isset(CSHJUNKIEQUOTES);
if isset(RCQUOTES) {
out.push('\''); let mut i = 0;
while i < bytes.len() {
let c = if bytes[i] as char == Dash {
i += 1;
'-'
} else if bytes[i] == Meta && i + 1 < bytes.len() {
let dec = bytes[i + 1] ^ 32;
i += 2;
dec as char
} else {
let dec = bytes[i];
i += 1;
dec as char
};
if c == '\'' {
out.push('\''); } else if c == '\n' && csh_junkie {
out.push('\\'); }
out.push(c); }
out.push('\''); } else {
let mut inquote = false; let mut i = 0;
while i < bytes.len() {
let c = if bytes[i] as char == Dash {
i += 1;
'-' } else if bytes[i] == Meta && i + 1 < bytes.len() {
let dec = bytes[i + 1] ^ 32; i += 2;
dec as char
} else {
let dec = bytes[i]; i += 1;
dec as char
};
if c == '\'' {
if inquote {
out.push('\''); inquote = false; }
out.push('\\'); out.push('\''); } else {
if !inquote {
out.push('\''); inquote = true; }
if c == '\n' && csh_junkie {
out.push('\\'); }
out.push(c); }
}
if inquote {
out.push('\''); }
}
out }
pub fn dquotedztrdup(s: &str) -> String {
let mut out = String::with_capacity(s.len() * 4 + 2);
let bytes = s.as_bytes();
if isset(CSHJUNKIEQUOTES) {
let mut inquote = false;
let mut i = 0;
while i < bytes.len() {
let c = if bytes[i] == Meta && i + 1 < bytes.len() {
i += 2;
(bytes[i - 1] ^ 32) as char
} else {
i += 1;
bytes[i - 1] as char
};
match c {
'"' | '$' | '`' => {
if inquote {
out.push('"');
inquote = false;
}
out.push('\\');
out.push(c);
}
_ => {
if !inquote {
out.push('"');
inquote = true;
}
if c == '\n' {
out.push('\\');
}
out.push(c);
}
}
}
if inquote {
out.push('"');
}
} else {
out.push('"');
let mut pending = false;
let mut i = 0;
while i < bytes.len() {
let c = if bytes[i] == Meta && i + 1 < bytes.len() {
i += 2;
(bytes[i - 1] ^ 32) as char
} else {
i += 1;
bytes[i - 1] as char
};
match c {
'\\' => {
if pending {
out.push('\\');
}
out.push('\\');
pending = true;
}
'"' | '$' | '`' => {
if pending {
out.push('\\');
}
out.push('\\');
out.push(c);
pending = false;
}
other => {
out.push(other);
pending = false;
}
}
}
if pending {
out.push('\\');
}
out.push('"');
}
metafy(&out)
}
pub fn dquotedzputs(s: &str) -> String {
dquotedztrdup(s) }
pub fn ucs4toutf8(wval: u32) -> Option<String> {
let len: usize = if wval < 0x80 {
1
}
else if wval < 0x800 {
2
}
else if wval < 0x10000 {
3
}
else if wval < 0x200000 {
4
}
else if wval < 0x4000000 {
5
}
else if wval < 0x80000000 {
6
}
else {
zerr("character not in range"); return None; };
let mut buf = [0u8; 6];
let mut w = wval;
match len {
1 => {
buf[0] = w as u8;
} n => {
for i in (1..n).rev() {
buf[i] = ((w & 0x3f) as u8) | 0x80;
w >>= 6;
}
buf[0] = (w as u8) | (((0xfcu32 << (6 - n)) & 0xfc) as u8);
}
}
Some(String::from_utf8_lossy(&buf[..len]).into_owned())
}
pub fn ucs4tomb(wval: u32, buf: &mut [u8]) -> i32 {
extern "C" {
fn wctomb(s: *mut libc::c_char, wc: libc::wchar_t) -> libc::c_int;
}
let mut local = [0 as libc::c_char; 16];
let count = unsafe { wctomb(local.as_mut_ptr(), wval as libc::wchar_t) };
if count < 0 {
zerr("character not in range");
return -1;
}
let n = count as usize;
if n > buf.len() {
zerr("character not in range");
return -1;
}
for i in 0..n {
buf[i] = local[i] as u8;
}
count
}
pub fn getkeystring(s: &str) -> (String, usize) {
let mut result = String::new();
let mut chars = s.chars().peekable();
let mut consumed = 0;
while let Some(c) = chars.next() {
consumed += c.len_utf8();
if c != '\\' {
result.push(c);
continue;
}
match chars.next() {
Some('n') => {
result.push('\n');
consumed += 1;
}
Some('t') => {
result.push('\t');
consumed += 1;
}
Some('r') => {
result.push('\r');
consumed += 1;
}
Some('e') | Some('E') => {
result.push('\x1b');
consumed += 1;
}
Some('a') => {
result.push('\x07');
consumed += 1;
}
Some('b') => {
result.push('\x08');
consumed += 1;
}
Some('f') => {
result.push('\x0c');
consumed += 1;
}
Some('v') => {
result.push('\x0b');
consumed += 1;
}
Some('\\') => {
result.push('\\');
consumed += 1;
}
Some('\'') => {
result.push('\'');
consumed += 1;
}
Some('"') => {
result.push('"');
consumed += 1;
}
Some('x') => {
consumed += 1;
let mut hex = String::new();
for _ in 0..2 {
if let Some(&c) = chars.peek() {
if c.is_ascii_hexdigit() {
hex.push(chars.next().unwrap());
consumed += 1;
} else {
break;
}
}
}
if let Ok(val) = u8::from_str_radix(&hex, 16) {
result.push(val as char);
}
}
Some('u') => {
consumed += 1;
let mut hex = String::new();
for _ in 0..4 {
if let Some(&c) = chars.peek() {
if c.is_ascii_hexdigit() {
hex.push(chars.next().unwrap());
consumed += 1;
} else {
break;
}
}
}
if let Ok(val) = u32::from_str_radix(&hex, 16) {
if let Some(c) = char::from_u32(val) {
result.push(c);
}
}
}
Some('U') => {
consumed += 1;
let mut hex = String::new();
for _ in 0..8 {
if let Some(&c) = chars.peek() {
if c.is_ascii_hexdigit() {
hex.push(chars.next().unwrap());
consumed += 1;
} else {
break;
}
}
}
if let Ok(val) = u32::from_str_radix(&hex, 16) {
if let Some(c) = char::from_u32(val) {
result.push(c);
}
}
}
Some(c @ '0'..='7') => {
consumed += 1;
let mut oct = String::new();
oct.push(c);
for _ in 0..2 {
if let Some(&c) = chars.peek() {
if ('0'..='7').contains(&c) {
oct.push(chars.next().unwrap());
consumed += 1;
} else {
break;
}
}
}
if let Ok(val) = u8::from_str_radix(&oct, 8) {
result.push(val as char);
}
}
Some('c') => {
consumed += 1;
if let Some(c) = chars.next() {
consumed += 1;
result.push((c as u8 & 0x1f) as char);
}
}
Some(mod_letter @ ('C' | 'M')) => {
consumed += 1;
let mut control = mod_letter == 'C';
let mut meta = mod_letter == 'M';
loop {
if chars.peek() == Some(&'-') {
chars.next();
consumed += 1;
continue;
}
let mut iter_clone = chars.clone();
if iter_clone.next() == Some('\\') {
if let Some(nx) = iter_clone.next() {
if nx == 'C' || nx == 'M' {
chars.next(); chars.next(); consumed += 2;
if nx == 'C' {
control = true;
} else {
meta = true;
}
continue;
}
}
}
break;
}
let base: Option<char> = if chars.peek() == Some(&'\\') {
chars.next();
consumed += 1;
match chars.next() {
Some('n') => {
consumed += 1;
Some('\n')
}
Some('t') => {
consumed += 1;
Some('\t')
}
Some('r') => {
consumed += 1;
Some('\r')
}
Some('a') => {
consumed += 1;
Some('\x07')
}
Some('b') => {
consumed += 1;
Some('\x08')
}
Some('e') | Some('E') => {
consumed += 1;
Some('\x1b')
}
Some('f') => {
consumed += 1;
Some('\x0c')
}
Some('v') => {
consumed += 1;
Some('\x0b')
}
Some('\\') => {
consumed += 1;
Some('\\')
}
Some('\'') => {
consumed += 1;
Some('\'')
}
Some('"') => {
consumed += 1;
Some('"')
}
Some(other) => {
consumed += 1;
Some(other)
}
None => None,
}
} else {
chars.next().inspect(|c| {
consumed += c.len_utf8();
})
};
if let Some(ch) = base {
let mut byte = ch as u32;
if control {
if byte == '?' as u32 {
byte = 0x7f;
} else {
byte &= 0x9f;
}
}
if meta {
byte |= 0x80;
}
if let Some(c) = char::from_u32(byte) {
result.push(c);
}
}
}
Some(c) => {
consumed += 1;
result.push('\\');
result.push(c);
}
None => {
result.push('\\');
}
}
}
(result, consumed)
}
pub fn strpfx(s: &str, t: &str) -> bool {
t.starts_with(s)
}
pub fn strsfx(s: &str, t: &str) -> bool {
t.ends_with(s)
}
pub fn upchdir(n: usize) -> io::Result<()> {
let mut path = String::new();
for i in 0..n {
if i > 0 {
path.push('/');
}
path.push_str("..");
}
std::env::set_current_dir(&path)?;
Ok(())
}
pub fn init_dirsav() -> dirsav {
dirsav {
dirfd: -1,
level: 0,
dev: 0,
ino: 0, dirname: std::env::current_dir()
.ok()
.map(|p| p.to_string_lossy().to_string()),
}
}
pub fn lchdir(path: &str) -> io::Result<()> {
let resolved = if path.starts_with('/') {
PathBuf::from(path)
} else {
let cwd = std::env::current_dir()?;
cwd.join(path)
};
std::env::set_current_dir(&resolved)?;
Ok(())
}
pub fn restoredir(d: &mut dirsav) -> i32 {
if let Some(name) = d.dirname.as_ref() {
if name.starts_with('/') {
return match std::env::set_current_dir(name) {
Ok(_) => 0,
Err(_) => -1,
};
}
}
let mut err: i32 = 0;
#[cfg(unix)]
if d.dirfd >= 0 {
let rc = unsafe { libc::fchdir(d.dirfd) };
if rc == 0 {
if d.dirname.is_none() {
return 0;
}
let name = d.dirname.as_ref().unwrap();
if std::env::set_current_dir(name).is_err() {
unsafe { libc::close(d.dirfd) };
d.dirfd = -1;
err = -2;
}
} else {
unsafe { libc::close(d.dirfd) };
d.dirfd = -1;
err = -1;
}
} else if d.level > 0 {
let _ = upchdir(d.level as usize);
} else if d.level < 0 {
err = -1;
}
if (d.dev != 0 || d.ino != 0) && err == 0 {
if let Ok(meta) = fs::metadata(".") {
if meta.ino() != d.ino || meta.dev() != d.dev {
err = -1;
}
} else {
err = -1;
}
}
err
}
pub fn privasserted() -> bool {
#[cfg(unix)]
{
if unsafe { libc::geteuid() } == 0 {
return true;
}
}
#[cfg(all(target_os = "linux", feature = "libcap"))]
{
if let Ok(text) = crate::ported::modules::cap::cap_get_proc() {
if !text.is_empty() && text != "=" {
return true;
}
}
}
false
}
pub fn mode_to_octal(mode: u32) -> i32 {
#[cfg(not(unix))]
{
let mut o: i32 = 0;
if mode & 0o4000 != 0 {
o |= 0o4000;
} if mode & 0o2000 != 0 {
o |= 0o2000;
} if mode & 0o1000 != 0 {
o |= 0o1000;
} if mode & 0o0400 != 0 {
o |= 0o0400;
} if mode & 0o0200 != 0 {
o |= 0o0200;
} if mode & 0o0100 != 0 {
o |= 0o0100;
} if mode & 0o0040 != 0 {
o |= 0o0040;
} if mode & 0o0020 != 0 {
o |= 0o0020;
} if mode & 0o0010 != 0 {
o |= 0o0010;
} if mode & 0o0004 != 0 {
o |= 0o0004;
} if mode & 0o0002 != 0 {
o |= 0o0002;
} if mode & 0o0001 != 0 {
o |= 0o0001;
} return o; }
#[cfg(unix)]
{
let mut m: i32 = 0;
if mode & S_ISUID as u32 != 0 {
m |= 0o4000;
} if mode & S_ISGID as u32 != 0 {
m |= 0o2000;
} if mode & S_ISVTX as u32 != 0 {
m |= 0o1000;
} if mode & S_IRUSR as u32 != 0 {
m |= 0o0400;
} if mode & S_IWUSR as u32 != 0 {
m |= 0o0200;
} if mode & S_IXUSR as u32 != 0 {
m |= 0o0100;
} if mode & S_IRGRP as u32 != 0 {
m |= 0o0040;
} if mode & S_IWGRP as u32 != 0 {
m |= 0o0020;
} if mode & S_IXGRP as u32 != 0 {
m |= 0o0010;
} if mode & S_IROTH as u32 != 0 {
m |= 0o0004;
} if mode & S_IWOTH as u32 != 0 {
m |= 0o0002;
} if mode & S_IXOTH as u32 != 0 {
m |= 0o0001;
} m }
}
pub fn mailstat(path: &str, st: &mut libc::stat) -> i32 {
let c_path = match CString::new(path) {
Ok(c) => c,
Err(_) => return -1,
};
let i = unsafe { libc::stat(c_path.as_ptr(), st as *mut _) }; if i != 0 || (st.st_mode & libc::S_IFMT) != libc::S_IFDIR {
return i; }
st.st_nlink = 1; st.st_mode &= !libc::S_IFDIR; st.st_mode |= libc::S_IFREG; st.st_size = 0; st.st_blocks = 0; let cur_path = match CString::new(format!("{}/cur", path)) {
Ok(c) => c,
Err(_) => return 0,
};
let mut sub: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::stat(cur_path.as_ptr(), &mut sub) } != 0 || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
{
return 0; }
st.st_atime = sub.st_atime; let tmp_path = match CString::new(format!("{}/tmp", path)) {
Ok(c) => c,
Err(_) => return 0,
};
if unsafe { libc::stat(tmp_path.as_ptr(), &mut sub) } != 0 || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
{
return 0; }
st.st_mtime = sub.st_mtime; let new_path = match CString::new(format!("{}/new", path)) {
Ok(c) => c,
Err(_) => return 0,
};
if unsafe { libc::stat(new_path.as_ptr(), &mut sub) } != 0 || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
{
return 0; }
st.st_mtime = sub.st_mtime; let mut atime: libc::time_t = 0; let mut mtime: libc::time_t = 0; for sub_name in ["new", "cur"] {
let dir = format!("{}/{}", path, sub_name);
let entries = match fs::read_dir(&dir) {
Ok(e) => e,
Err(_) => return 0,
};
for entry in entries.flatten() {
let name = entry.file_name();
let name_bytes = name.as_encoded_bytes();
if name_bytes.first() == Some(&b'.') {
continue;
}
let entry_path = match CString::new(entry.path().to_string_lossy().as_bytes()) {
Ok(c) => c,
Err(_) => continue,
};
let mut entry_st: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::stat(entry_path.as_ptr(), &mut entry_st) } != 0 {
continue;
}
st.st_size += entry_st.st_size; st.st_blocks += 1; if entry_st.st_atime != entry_st.st_mtime && entry_st.st_atime > atime {
atime = entry_st.st_atime;
}
if entry_st.st_mtime > mtime {
mtime = entry_st.st_mtime;
}
}
}
if atime != 0 {
st.st_atime = atime;
}
if mtime != 0 {
st.st_mtime = mtime;
}
0 }
pub static mut SCRIPT_NAME: Option<String> = None;
pub static mut SCRIPT_FILENAME: Option<String> = None;
static SCRIPTNAME: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
static ARGZERO: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
static SCRIPTFILENAME: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
static POSIXZERO: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
#[allow(non_upper_case_globals)]
pub static errflag: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
static NOERRS: std::sync::OnceLock<Mutex<i32>> = std::sync::OnceLock::new();
static LINENO: std::sync::OnceLock<Mutex<i32>> = std::sync::OnceLock::new();
static SHINSTDIN_OPT: std::sync::OnceLock<Mutex<bool>> = std::sync::OnceLock::new();
pub const ERRFLAG_ERROR: i32 = 1;
pub static INCOMPFUNC: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static RESETNEEDED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static WINCHANGED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub const GETKEY_OCTAL_ESC: u32 = 1 << 0; pub const GETKEY_EMACS: u32 = 1 << 1; pub const GETKEY_CTRL: u32 = 1 << 2; pub const GETKEY_BACKSLASH_C: u32 = 1 << 3;
pub const GETKEYS_PRINT: u32 = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_BACKSLASH_C;
pub const GETKEYS_ECHO: u32 = GETKEY_BACKSLASH_C;
static ATTACHTTY_EP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
static FDTABLE: std::sync::OnceLock<Mutex<Vec<i32>>> = std::sync::OnceLock::new();
pub static MAX_ZSH_FD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
pub static FDTABLE_FLOCKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub fn set_scriptname(name: Option<String>) {
*scriptname_lock().lock().unwrap() = name;
}
pub fn scriptname_get() -> Option<String> {
scriptname_lock().lock().unwrap().clone()
}
pub fn set_scriptfilename(name: Option<String>) {
*scriptfilename_lock().lock().unwrap() = name;
}
pub fn scriptfilename_get() -> Option<String> {
scriptfilename_lock().lock().unwrap().clone()
}
pub fn locallevel() -> i32 {
LOCALLEVEL.load(Ordering::Relaxed)
}
pub fn inc_locallevel() {
LOCALLEVEL.fetch_add(1, Ordering::Relaxed);
}
pub fn dec_locallevel() {
LOCALLEVEL.fetch_sub(1, Ordering::Relaxed);
}
pub fn set_argzero(name: Option<String>) {
let mut posix_lock = posixzero_lock().lock().unwrap();
if posix_lock.is_none() {
*posix_lock = name.clone();
}
drop(posix_lock);
*argzero_lock().lock().unwrap() = name;
}
pub fn argzero() -> Option<String> {
argzero_lock().lock().unwrap().clone()
}
pub fn set_posixzero(name: Option<String>) {
*posixzero_lock().lock().unwrap() = name;
}
pub fn posixzero() -> Option<String> {
posixzero_lock().lock().unwrap().clone()
}
pub fn set_noerrs(v: i32) {
*noerrs_lock().lock().unwrap() = v;
}
pub fn set_locallevel(v: i32) {
LOCALLEVEL.store(v, Ordering::Relaxed);
}
pub fn set_lineno(v: i32) {
*lineno_lock().lock().unwrap() = v;
}
pub fn set_shinstdin(v: bool) {
*shinstdin_lock().lock().unwrap() = v;
}
pub fn qflag_quotetype(count: u32) -> i32 {
match count {
0 => QT_NONE,
1 => QT_BACKSLASH,
2 => QT_SINGLE,
3 => QT_DOUBLE,
_ => QT_DOLLARS,
}
}
fn ispecial(c: char) -> bool {
matches!(
c,
'#' | '$' | '^' | '*' | '(' | ')' | '=' | '|' | '{' | '}' | '[' | ']' | '`' | '<' | '>' | '?' | '~' | ';' | '&' | '\n' | '\t' | ' ' | '\\' | '\'' | '"' )
}
pub fn convbase(val: i64, base: u32) -> String {
convbase_param(val, base)
}
pub fn statuidprint(uid: u32) -> Option<String> {
#[cfg(unix)]
{
let pwd = unsafe { libc::getpwuid(uid) };
if pwd.is_null() {
return None;
}
let name = unsafe { std::ffi::CStr::from_ptr((*pwd).pw_name) };
name.to_str().ok().map(|s| s.to_string())
}
#[cfg(not(unix))]
{
let _ = uid;
None
}
}
pub fn gethostname() -> String {
#[cfg(unix)]
{
let mut buf = vec![0u8; 256];
unsafe {
if libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) == 0 {
let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
return String::from_utf8_lossy(&buf[..len]).to_string();
}
}
}
getsparam("HOST")
.or_else(|| getsparam("HOSTNAME"))
.unwrap_or_else(|| "localhost".to_string())
}
static PREPROMPT_FNS: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
pub fn setenv(name: &str, value: &str) {
std::env::set_var(name, value);
}
pub fn unsetenv(name: &str) {
std::env::remove_var(name);
}
pub static TIMED_FNS: Mutex<Vec<(i64, fn())>> = Mutex::new(Vec::new());
pub fn getuid() -> u32 {
#[cfg(unix)]
unsafe {
libc::getuid()
}
#[cfg(not(unix))]
0
}
pub fn geteuid() -> u32 {
#[cfg(unix)]
unsafe {
libc::geteuid()
}
#[cfg(not(unix))]
0
}
pub fn getgid() -> u32 {
#[cfg(unix)]
unsafe {
libc::getgid()
}
#[cfg(not(unix))]
0
}
pub fn getegid() -> u32 {
#[cfg(unix)]
unsafe {
libc::getegid()
}
#[cfg(not(unix))]
0
}
pub fn getpid() -> i32 {
std::process::id() as i32
}
pub fn getppid() -> i32 {
#[cfg(unix)]
unsafe {
libc::getppid()
}
#[cfg(not(unix))]
0
}
pub fn getkeystring_with(s: &str, how: u32) -> (String, usize) {
let mut result = String::new();
let mut chars = s.chars().peekable();
let mut consumed = 0;
while let Some(c) = chars.next() {
consumed += c.len_utf8();
if c != '\\' {
result.push(c);
continue;
}
match chars.next() {
Some('n') => {
result.push('\n');
consumed += 1;
}
Some('t') => {
result.push('\t');
consumed += 1;
}
Some('r') => {
result.push('\r');
consumed += 1;
}
Some('e') | Some('E') => {
result.push('\x1b');
consumed += 1;
}
Some('a') => {
result.push('\x07');
consumed += 1;
}
Some('b') => {
result.push('\x08');
consumed += 1;
}
Some('f') => {
result.push('\x0c');
consumed += 1;
}
Some('v') => {
result.push('\x0b');
consumed += 1;
}
Some('\\') => {
result.push('\\');
consumed += 1;
}
Some('x') => {
consumed += 1;
let mut hex = String::new();
for _ in 0..2 {
if let Some(&c) = chars.peek() {
if c.is_ascii_hexdigit() {
hex.push(chars.next().unwrap());
consumed += 1;
} else {
break;
}
}
}
if let Ok(val) = u8::from_str_radix(&hex, 16) {
result.push(val as char);
if (how & crate::ported::zsh_h::GETKEY_PRINTF_PERCENT as u32) != 0
&& val == b'%'
{
result.push('%');
}
}
}
Some('u') => {
consumed += 1;
let mut hex = String::new();
for _ in 0..4 {
if let Some(&c) = chars.peek() {
if c.is_ascii_hexdigit() {
hex.push(chars.next().unwrap());
consumed += 1;
} else {
break;
}
}
}
if let Ok(val) = u32::from_str_radix(&hex, 16) {
if let Some(ch) = char::from_u32(val) {
result.push(ch);
}
}
}
Some('U') => {
consumed += 1;
let mut hex = String::new();
for _ in 0..8 {
if let Some(&c) = chars.peek() {
if c.is_ascii_hexdigit() {
hex.push(chars.next().unwrap());
consumed += 1;
} else {
break;
}
}
}
if let Ok(val) = u32::from_str_radix(&hex, 16) {
if let Some(ch) = char::from_u32(val) {
result.push(ch);
}
}
}
Some('0') if (how & GETKEY_OCTAL_ESC) == 0 => {
consumed += 1;
let mut oct = String::new();
for _ in 0..3 {
if let Some(&c) = chars.peek() {
if ('0'..='7').contains(&c) {
oct.push(chars.next().unwrap());
consumed += 1;
} else {
break;
}
}
}
let val: u8 = if oct.is_empty() {
0 } else {
u8::from_str_radix(&oct, 8).unwrap_or(0)
};
result.push(val as char);
if (how & crate::ported::zsh_h::GETKEY_PRINTF_PERCENT as u32) != 0
&& val == b'%'
{
result.push('%');
}
}
Some(d) if d.is_digit(8) && (how & GETKEY_OCTAL_ESC) != 0 => {
consumed += 1;
let mut oct = String::from(d);
for _ in 0..2 {
if let Some(&c) = chars.peek() {
if c.is_digit(8) {
oct.push(chars.next().unwrap());
consumed += 1;
} else {
break;
}
}
}
if let Ok(val) = u8::from_str_radix(&oct, 8) {
result.push(val as char);
if (how & crate::ported::zsh_h::GETKEY_PRINTF_PERCENT as u32) != 0
&& val == b'%'
{
result.push('%');
}
}
}
Some(c) => {
consumed += 1;
if c == 'c' && (how & GETKEY_BACKSLASH_C) != 0 {
GETKEY_TRUNCATED.with(|cell| cell.set(true));
break;
}
if (how & GETKEY_EMACS) == 0 {
result.push('\\');
}
result.push(c);
}
None => {
result.push('\\');
}
}
}
(result, consumed)
}
thread_local! {
pub static GETKEY_TRUNCATED: std::cell::Cell<bool> =
const { std::cell::Cell::new(false) };
}
pub fn getkey_truncated_take() -> bool {
GETKEY_TRUNCATED.with(|c| {
let v = c.get();
c.set(false);
v
})
}
pub fn fdtable_get(fd: i32) -> i32 {
if fd < 0 {
return FDT_UNUSED;
}
let g = fdtable_lock().lock().unwrap();
g.get(fd as usize).copied().unwrap_or(FDT_UNUSED)
}
pub fn fdtable_set(fd: i32, kind: i32) {
if fd < 0 {
return;
}
let mut g = fdtable_lock().lock().unwrap();
if (fd as usize) >= g.len() {
g.resize((fd as usize) + 1, FDT_UNUSED);
}
g[fd as usize] = kind;
let cur = MAX_ZSH_FD.load(Ordering::Relaxed);
if fd > cur {
MAX_ZSH_FD.store(fd, Ordering::Relaxed);
}
}
#[inline]
pub fn imeta_byte(b: u8) -> bool {
b == 0 || (0x83..=0xa2).contains(&b)
}
pub(crate) fn base64_decode(s: &str) -> Vec<u8> {
let decode_char = |c: u8| -> Option<u8> {
match c {
b'A'..=b'Z' => Some(c - b'A'),
b'a'..=b'z' => Some(c - b'a' + 26),
b'0'..=b'9' => Some(c - b'0' + 52),
b'+' => Some(62),
b'/' => Some(63),
_ => None,
}
};
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(s.len() / 4 * 3);
let mut i = 0;
while i + 4 <= bytes.len() {
let chunk = &bytes[i..i + 4];
let pad = chunk.iter().filter(|&&c| c == b'=').count();
let v0 = decode_char(chunk[0]).unwrap_or(0) as u32;
let v1 = decode_char(chunk[1]).unwrap_or(0) as u32;
let v2 = decode_char(chunk[2]).unwrap_or(0) as u32;
let v3 = decode_char(chunk[3]).unwrap_or(0) as u32;
let n = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3;
out.push(((n >> 16) & 0xff) as u8);
if pad < 2 {
out.push(((n >> 8) & 0xff) as u8);
}
if pad < 1 {
out.push((n & 0xff) as u8);
}
i += 4;
}
out
}
fn scriptname_lock() -> &'static Mutex<Option<String>> {
SCRIPTNAME.get_or_init(|| Mutex::new(None))
}
fn argzero_lock() -> &'static Mutex<Option<String>> {
ARGZERO.get_or_init(|| Mutex::new(None))
}
fn scriptfilename_lock() -> &'static Mutex<Option<String>> {
SCRIPTFILENAME.get_or_init(|| Mutex::new(None))
}
fn posixzero_lock() -> &'static Mutex<Option<String>> {
POSIXZERO.get_or_init(|| Mutex::new(None))
}
fn noerrs_lock() -> &'static Mutex<i32> {
NOERRS.get_or_init(|| Mutex::new(0))
}
fn lineno_lock() -> &'static Mutex<i32> {
LINENO.get_or_init(|| Mutex::new(0))
}
fn shinstdin_lock() -> &'static Mutex<bool> {
SHINSTDIN_OPT.get_or_init(|| Mutex::new(false))
}
fn fdtable_lock() -> &'static Mutex<Vec<i32>> {
FDTABLE.get_or_init(|| Mutex::new(Vec::new()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn getaparam_reads_reply_from_paramtab_not_env() {
let _g = crate::test_util::global_state_lock();
let saved = getaparam("reply");
let payload = vec!["abbreviated".to_string(), "11".to_string()];
let _ = setaparam("reply", payload.clone());
assert_eq!(
getaparam("reply"),
Some(payload),
"getaparam(\"reply\") must return paramtab array"
);
let _ = setaparam("reply", saved.unwrap_or_default());
}
#[test]
fn finddir_uses_paramtab_home_not_env() {
let _g = crate::test_util::global_state_lock();
let mut pm = crate::ported::zsh_h::param::default();
let saved = crate::ported::params::homegetfn(&pm);
let sentinel = "/tmp/zshrs-finddir-pin".to_string();
homesetfn(&mut pm, sentinel.clone());
let abbrev = finddir(&format!("{}/x", sentinel));
assert_eq!(
abbrev.as_deref(),
Some("~/x"),
"finddir must consult canonical HOME (got {:?})",
abbrev
);
homesetfn(&mut pm, saved);
}
#[test]
fn test_sepsplit() {
let _g = crate::test_util::global_state_lock();
assert_eq!(sepsplit("a:b:c", Some(":"), false), vec!["a", "b", "c"]);
assert_eq!(sepsplit("a::b", Some(":"), false), vec!["a", "b"]);
assert_eq!(sepsplit("a::b", Some(":"), true), vec!["a", "", "b"]);
}
#[test]
fn test_unmetafy_no_meta_byte_passes_through() {
let _g = crate::test_util::global_state_lock();
let mut buf = b"hello".to_vec();
let n = unmetafy(&mut buf);
assert_eq!(n, 5);
assert_eq!(&buf, b"hello");
}
#[test]
fn test_unmetafy_collapses_meta_escapes() {
let _g = crate::test_util::global_state_lock();
let mut buf = vec![0x83, b'a' ^ 32];
let n = unmetafy(&mut buf);
assert_eq!(n, 1);
assert_eq!(buf, vec![b'a']);
}
#[test]
fn test_unmetafy_mixed_prefix_then_meta() {
let _g = crate::test_util::global_state_lock();
let mut buf = vec![b'X', b'Y', 0x83, 0xFF ^ 32, b'Z'];
let n = unmetafy(&mut buf);
assert_eq!(n, 4);
assert_eq!(buf, vec![b'X', b'Y', 0xFF, b'Z']);
}
#[test]
fn test_unmetafy_returns_self_value() {
let _g = crate::test_util::global_state_lock();
let mut buf = vec![
b'A',
0x83,
b'B' ^ 32, 0x83,
b'C' ^ 32, b'D',
];
let n = unmetafy(&mut buf);
assert_eq!(n, 4);
assert_eq!(buf, b"ABCD".to_vec());
}
#[test]
fn test_imeta_byte_threshold() {
let _g = crate::test_util::global_state_lock();
assert!(imeta_byte(0x00), "c:4195 — NUL is IMETA");
assert!(!imeta_byte(0x82), "0x82 is NOT IMETA (below Meta)");
assert!(imeta_byte(Meta), "c:4196 — Meta (0x83) is IMETA");
assert!(imeta_byte(0xa2), "c:4197 — Marker (0xa2) is IMETA");
assert!(imeta_byte(0xa1), "c:4200 — Nularg (0xa1) is IMETA");
assert!(!imeta_byte(0xa3), "0xa3 NOT IMETA (above Marker)");
assert!(!imeta_byte(0xc3), "0xc3 NOT IMETA (UTF-8 'é' lead)");
assert!(!imeta_byte(0xFF), "0xFF NOT IMETA");
}
#[test]
fn test_meta_constant_value() {
let _g = crate::test_util::global_state_lock();
assert_eq!(Meta, 0x83);
}
#[test]
#[cfg(unix)]
fn test_mode_to_octal_canonical_bits() {
let _g = crate::test_util::global_state_lock();
let mode = (S_IRUSR | S_IWUSR | S_IXUSR) as u32;
assert_eq!(mode_to_octal(mode), 0o700);
let all = (S_IRUSR | S_IWUSR | S_IXUSR) as u32 * (1 + 8 + 64);
let m = (S_IRUSR
| S_IWUSR
| S_IXUSR
| S_IRGRP
| S_IWGRP
| S_IXGRP
| S_IROTH
| S_IWOTH
| S_IXOTH) as u32;
assert_eq!(mode_to_octal(m), 0o777);
let _ = all;
}
#[test]
#[cfg(unix)]
fn test_mode_to_octal_setuid_setgid_sticky() {
let _g = crate::test_util::global_state_lock();
assert_eq!(mode_to_octal(S_ISUID as u32), 0o4000);
assert_eq!(mode_to_octal(S_ISGID as u32), 0o2000);
assert_eq!(mode_to_octal(S_ISVTX as u32), 0o1000);
let all = (S_ISUID | S_ISGID | S_ISVTX) as u32;
assert_eq!(mode_to_octal(all), 0o7000);
}
#[test]
#[cfg(unix)]
fn test_mailstat_plain_file_returns_native_stat() {
let _g = crate::test_util::global_state_lock();
let mut st: libc::stat = unsafe { std::mem::zeroed() };
let rc = mailstat("/etc/hosts", &mut st);
if rc == 0 {
assert_eq!(
st.st_nlink as u64,
fs::metadata("/etc/hosts").unwrap().nlink()
);
}
}
#[test]
fn test_mailstat_nonexistent_returns_neg1() {
let _g = crate::test_util::global_state_lock();
let mut st: libc::stat = unsafe { std::mem::zeroed() };
assert_eq!(mailstat("/nonexistent/path/does/not/exist", &mut st), -1);
}
#[test]
fn test_mailstat_directory_without_maildir_subdirs() {
let _g = crate::test_util::global_state_lock();
let mut st: libc::stat = unsafe { std::mem::zeroed() };
let rc = mailstat("/tmp", &mut st);
assert_eq!(rc, 0);
assert_eq!(st.st_nlink, 1);
assert_eq!(st.st_size, 0);
assert_eq!(st.st_blocks, 0);
#[cfg(unix)]
{
assert_eq!(st.st_mode & libc::S_IFDIR, 0);
assert_ne!(st.st_mode & libc::S_IFREG, 0);
}
}
#[test]
fn test_dupstrpfx_byte_counted() {
let _g = crate::test_util::global_state_lock(); assert_eq!(dupstrpfx("hello", 3), "hel"); assert_eq!(dupstrpfx("hi", 10), "hi"); assert_eq!(dupstrpfx("anything", 0), ""); }
#[test]
fn test_metafy_passes_through_ascii() {
let _g = crate::test_util::global_state_lock();
assert_eq!(metafy("hello"), "hello");
assert_eq!(metafy(""), "");
}
#[test]
fn test_metafy_imeta_predicate_matches_c_macro() {
let _g = crate::test_util::global_state_lock();
assert!(imeta_byte(0x00), "c:4195 — '\\0' IS imeta");
for b in 0x01u8..=0x82 {
assert!(!imeta_byte(b), "byte {:#x} should NOT be imeta", b);
}
for b in 0x83u8..=0xa2 {
assert!(imeta_byte(b), "c:4196-4200 — byte {:#x} IS imeta", b);
}
for b in 0xa3u8..=0xff {
assert!(
!imeta_byte(b),
"byte {:#x} should NOT be imeta (no c:4195-4201 assignment)",
b
);
}
}
#[test]
fn metafy_preserves_utf8_high_bytes_outside_imeta_range() {
let _g = crate::test_util::global_state_lock();
let input = std::str::from_utf8(&[0xC3, 0xA9]).unwrap();
let out = metafy(input);
let out_bytes = out.as_bytes();
assert_eq!(
out_bytes,
&[0xC3, 0xA9],
"c:4196-4200 — UTF-8 bytes 0xc3/0xa9 outside IMETA range must pass through"
);
}
#[test]
fn test_ztrcmp_meta_aware() {
let _g = crate::test_util::global_state_lock();
assert_eq!(ztrcmp("foo", "foo"), std::cmp::Ordering::Equal);
assert_eq!(ztrcmp("foo", "foz"), std::cmp::Ordering::Less);
assert_eq!(ztrcmp("foo", "foobar"), std::cmp::Ordering::Less);
let s_meta = unsafe { std::str::from_utf8_unchecked(&[0x83, b'a' ^ 32]) };
let s_plain = "a";
assert_eq!(ztrcmp(s_meta, s_plain), std::cmp::Ordering::Equal);
}
#[test]
fn test_skipwsep_skips_runs() {
let _g = crate::test_util::global_state_lock();
let (rest, n) = skipwsep(" x");
assert_eq!(rest, "x");
assert_eq!(n, 3);
let (rest, n) = skipwsep("foo");
assert_eq!(rest, "foo");
assert_eq!(n, 0);
let (rest, n) = skipwsep(" \t\nbar");
assert_eq!(rest, "bar");
assert_eq!(n, 3);
}
#[test]
fn test_imeta_macro_threshold() {
let _g = crate::test_util::global_state_lock(); inittyptab(); assert!(imeta(0x00), "c:4195 — NUL is IMETA"); assert!(imeta(Meta), "c:4196 — Meta (0x83) is IMETA"); assert!(imeta(0xa2), "c:4197 — Marker (0xa2) is IMETA"); assert!(
imeta(0x84),
"c:4199-4201 — Pound (0x84) is IMETA via ITOK range"
);
assert!(
imeta(0x9b),
"c:4199-4201 — Dash sentinel within ITOK range is IMETA"
);
assert!(!imeta(0x82), "c:4170 — 0x82 is ICNTRL, not IMETA"); assert!(!imeta(0xa3), "byte 0xa3 is past Marker — NOT IMETA");
assert!(
!imeta(0xff),
"byte 0xff is NOT IMETA (the prior `>= Meta` over-report)"
);
assert!(!imeta(b' '), "space is not IMETA");
assert!(!imeta(b'A'), "'A' is not IMETA");
}
#[test]
fn test_unmeta_routes_through_unmetafy() {
let _g = crate::test_util::global_state_lock();
assert_eq!(unmeta("plain"), "plain");
}
#[test]
fn test_iwsep_includes_newline() {
let _g = crate::test_util::global_state_lock(); assert!(iwsep(b'\n')); assert!(iwsep(b'\t')); assert!(iwsep(b' ')); assert!(!iwsep(b'a')); }
#[test]
fn test_mailstat_aggregates_maildir() {
let _g = crate::test_util::global_state_lock();
let tmp = std::env::temp_dir().join(format!("zshrs_mailstat_test_{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(tmp.join("cur")).unwrap();
fs::create_dir_all(tmp.join("new")).unwrap();
fs::create_dir_all(tmp.join("tmp")).unwrap();
let mut f = fs::File::create(tmp.join("new").join("msg1")).unwrap();
f.write_all(b"hello").unwrap();
let mut f = fs::File::create(tmp.join("new").join("msg2")).unwrap();
f.write_all(b"world!").unwrap();
let mut f = fs::File::create(tmp.join("cur").join("msg3")).unwrap();
f.write_all(b"third").unwrap();
let mut st: libc::stat = unsafe { std::mem::zeroed() };
let rc = mailstat(tmp.to_str().unwrap(), &mut st);
assert_eq!(rc, 0, "maildir should stat");
assert_eq!(st.st_blocks, 3, "3 messages total across new/ + cur/");
assert_eq!(st.st_size, 5 + 6 + 5, "5+6+5 bytes total");
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_spacesplit() {
let _g = crate::test_util::global_state_lock();
assert_eq!(spacesplit("a b c", false), vec!["a", "b", "c"]);
assert_eq!(spacesplit("a b", false), vec!["a", "b"]);
}
#[test]
fn test_sepjoin() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
sepjoin(&["a".into(), "b".into(), "c".into()], Some(":")),
"a:b:c"
);
assert_eq!(sepjoin(&["a".into(), "b".into()], None), "a b");
}
#[test]
fn test_isident() {
let _g = crate::test_util::global_state_lock(); assert!(isident("foo")); assert!(isident("_bar")); assert!(isident("baz123")); assert!(!isident("123abc")); assert!(!isident("foo-bar")); }
#[test]
fn test_nicechar() {
let _g = crate::test_util::global_state_lock();
assert_eq!(nicechar('\n'), "\\n");
assert_eq!(nicechar('\t'), "\\t");
assert_eq!(nicechar('a'), "a");
}
#[test]
fn test_quotedzputs_single_quote_wrap() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotedzputs("simple"), "simple");
assert_eq!(quotedzputs("has space"), "'has space'");
assert_eq!(quotedzputs("it's"), "'it'\\''s'");
}
#[test]
fn test_quotestring_backslash() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotestring("hello", QT_BACKSLASH), "hello");
assert_eq!(quotestring("has space", QT_BACKSLASH), "has\\ space");
assert_eq!(quotestring("$var", QT_BACKSLASH), "\\$var");
}
#[test]
fn quotestring_backslash_only_specchars_no_bang_in_default() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
quotestring("a!b", QT_BACKSLASH),
"a!b",
"c:228 — `!` is not in SPECCHARS; only bangchar+BANGHIST adds it"
);
assert_eq!(
quotestring("a,b", QT_BACKSLASH),
"a,b",
"c:228 — `,` not in SPECCHARS until makecommaspecial(1)"
);
assert_eq!(
quotestring("a^b", QT_BACKSLASH),
"a\\^b",
"c:228 — `^` is in SPECCHARS"
);
assert_eq!(
quotestring("a{b", QT_BACKSLASH),
"a\\{b",
"c:228 — open-brace is in SPECCHARS"
);
assert_eq!(
quotestring("a}b", QT_BACKSLASH),
"a\\}b",
"c:228 — close-brace is in SPECCHARS"
);
assert_eq!(
quotestring("a#b", QT_BACKSLASH),
"a\\#b",
"c:228 — `#` is the first char of SPECCHARS"
);
assert_eq!(
quotestring("a\\b", QT_BACKSLASH),
"a\\\\b",
"c:228 — `\\\\` in SPECCHARS"
);
}
#[test]
fn test_quotestring_single() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotestring("hello", QT_SINGLE), "'hello'");
assert_eq!(quotestring("it's", QT_SINGLE), "'it'\\''s'");
}
#[test]
fn test_quotestring_double() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotestring("hello", QT_DOUBLE), "\"hello\"");
assert_eq!(quotestring("say \"hi\"", QT_DOUBLE), "\"say \\\"hi\\\"\"");
}
#[test]
fn test_quotestring_dollars() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotestring("hello", QT_DOLLARS), "$'hello'");
assert_eq!(quotestring("line\nbreak", QT_DOLLARS), "$'line\\nbreak'");
assert_eq!(quotestring("tab\there", QT_DOLLARS), "$'tab\\there'");
}
#[test]
fn test_quotestring_pattern() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotestring("*.txt", QT_BACKSLASH_PATTERN), "\\*.txt");
assert_eq!(quotestring("file[1]", QT_BACKSLASH_PATTERN), "file\\[1\\]");
}
#[test]
fn test_quotetype_from_q_count() {
let _g = crate::test_util::global_state_lock();
assert_eq!(qflag_quotetype(1), QT_BACKSLASH);
assert_eq!(qflag_quotetype(2), QT_SINGLE);
assert_eq!(qflag_quotetype(3), QT_DOUBLE);
assert_eq!(qflag_quotetype(4), QT_DOLLARS);
}
#[test]
fn test_tulower_tuupper() {
let _g = crate::test_util::global_state_lock();
assert_eq!(tulower('A'), 'a');
assert_eq!(tuupper('a'), 'A');
assert_eq!(tulower('1'), '1');
}
#[test]
fn test_wordcount_ifs_default() {
let _g = crate::test_util::global_state_lock();
assert_eq!(wordcount("a b c", None, 0), 3);
assert_eq!(wordcount(" a b ", None, 0), 2);
assert_eq!(wordcount("", None, 0), 0);
assert_eq!(wordcount("foo", None, 0), 1);
}
#[test]
fn test_wordcount_with_explicit_sep() {
let _g = crate::test_util::global_state_lock();
assert_eq!(wordcount("a:b:c", Some(":"), 0), 3);
assert_eq!(wordcount("a::b", Some(":"), 1), 3);
assert_eq!(wordcount("a::b", Some(":"), 0), 2);
}
#[test]
fn test_ucs4tomb_ascii() {
let _g = crate::test_util::global_state_lock();
let mut buf = [0u8; 8];
let n = ucs4tomb('A' as u32, &mut buf);
assert_eq!(n, 1);
assert_eq!(buf[0], b'A');
}
#[test]
fn test_is_mb_niceformat_plain_ascii() {
let _g = crate::test_util::global_state_lock();
assert_eq!(is_mb_niceformat("hello world"), 0);
}
#[test]
fn test_is_mb_niceformat_with_control_char() {
let _g = crate::test_util::global_state_lock();
assert_eq!(is_mb_niceformat("a\tb"), 1);
assert_eq!(is_mb_niceformat("a\x07b"), 1);
}
#[test]
fn metafy_unmetafy_round_trips_for_ascii() {
let _g = crate::test_util::global_state_lock();
let s = "hello world";
let m = metafy(s);
let mut buf = m.into_bytes();
unmetafy(&mut buf);
assert_eq!(std::str::from_utf8(&buf).unwrap(), s);
}
#[test]
fn ztrlen_counts_ascii_one_per_byte() {
let _g = crate::test_util::global_state_lock();
assert_eq!(ztrlen(""), 0);
assert_eq!(ztrlen("a"), 1);
assert_eq!(ztrlen("hello"), 5);
}
#[test]
fn ztrlen_counts_meta_pair_as_one() {
let _g = crate::test_util::global_state_lock();
let meta = char::from_u32(Meta as u32).unwrap();
let s: String = [meta, '\x20'].iter().collect();
assert_eq!(
ztrlen(&s),
1,
"c:5141-5148 — Meta+X pair counts as ONE char, not two"
);
let mixed: String = ['a', meta, '\x20', 'b'].iter().collect();
assert_eq!(
ztrlen(&mixed),
3,
"c:5141 — three unmetafied chars from 'a' + Meta+X + 'b'"
);
}
#[cfg(unix)]
#[test]
fn setblock_fd_skips_regular_files_per_c_2599() {
let _g = crate::test_util::global_state_lock();
let dir = tempfile::TempDir::new().unwrap();
let f = fs::File::create(dir.path().join("regular")).unwrap();
let fd = f.as_raw_fd();
let (changed, mode) = setblock_fd(true, fd);
assert!(
!changed,
"c:2599 — regular files short-circuit; setblock_fd must NOT report a change"
);
assert_eq!(mode, -1, "c:2614 — `*modep = -1` for regular files");
}
#[cfg(unix)]
#[test]
fn setblock_fd_clears_o_nonblock_on_pipe() {
let _g = crate::test_util::global_state_lock();
let mut pipefd: [libc::c_int; 2] = [0; 2];
let r = unsafe { libc::pipe(pipefd.as_mut_ptr()) };
assert_eq!(r, 0, "pipe(2) must succeed");
let read_fd = pipefd[0];
let write_fd = pipefd[1];
let cur = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
unsafe {
libc::fcntl(read_fd, libc::F_SETFL, cur | libc::O_NONBLOCK);
}
let now = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
assert_ne!(
now & libc::O_NONBLOCK,
0,
"test setup: NONBLOCK should be set"
);
let (changed, _mode) = setblock_fd(true, read_fd);
assert!(
changed,
"c:2611 — turnonblocking=true on a NONBLOCK pipe must report state change"
);
let after = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
assert_eq!(
after & libc::O_NONBLOCK,
0,
"c:2611 — O_NONBLOCK must be cleared after turnonblocking=true"
);
unsafe {
libc::close(read_fd);
libc::close(write_fd);
}
}
#[cfg(unix)]
#[test]
fn setblock_stdin_enables_blocking_on_fd_zero() {
let _g = crate::test_util::global_state_lock();
let cur = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
if cur < 0 {
return;
}
unsafe {
libc::fcntl(0, libc::F_SETFL, cur | libc::O_NONBLOCK);
}
let after_set_nb = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
if after_set_nb & libc::O_NONBLOCK == 0 {
unsafe {
libc::fcntl(0, libc::F_SETFL, cur);
}
return;
}
setblock_stdin();
let after_setblock = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
assert_eq!(
after_setblock & libc::O_NONBLOCK,
0,
"c:2624 — setblock_stdin must CLEAR O_NONBLOCK (enable blocking)"
);
unsafe {
libc::fcntl(0, libc::F_SETFL, cur);
}
}
#[test]
fn zstrtol_underscore_base_10_parses_decimal() {
let _g = crate::test_util::global_state_lock();
let (v, rest) = zstrtol_underscore("12345", 10, false);
assert_eq!(v, 12345, "c:2471 — decimal accumulator");
assert_eq!(rest, "", "rest is empty after full consumption");
let (v, rest) = zstrtol_underscore("100abc", 10, false);
assert_eq!(v, 100);
assert_eq!(
rest, "abc",
"c:2467 — loop exits at first non-digit; rest carries on"
);
}
#[test]
fn zstrtol_underscore_base_zero_autodetects_prefix() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zstrtol_underscore("0xff", 0, false).0,
255,
"c:2455 — 0x → base 16"
);
assert_eq!(zstrtol_underscore("0XFF", 0, false).0, 255);
assert_eq!(
zstrtol_underscore("0b1010", 0, false).0,
10,
"c:2457 — 0b → base 2"
);
assert_eq!(
zstrtol_underscore("0777", 0, false).0,
511,
"c:2460 — leading 0 → octal (no OCTALZEROES gate for zstrtol)"
);
assert_eq!(zstrtol_underscore("12345", 0, false).0, 12345);
}
#[test]
fn zstrtol_underscore_handles_sign_chars() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zstrtol_underscore("-42", 10, false).0,
-42,
"c:2447 — leading `-` → negate"
);
assert_eq!(
zstrtol_underscore("+42", 10, false).0,
42,
"c:2449 — leading `+` consumed, no negation"
);
assert_eq!(
zstrtol_underscore(" -100", 10, false).0,
-100,
"c:2444 — leading whitespace skipped, then sign"
);
}
#[test]
fn zstrtol_underscore_base_16_accepts_letters() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zstrtol_underscore("ff", 16, false).0,
255,
"c:2485 — base-16 'ff' → 255"
);
assert_eq!(
zstrtol_underscore("FF", 16, false).0,
255,
"c:2485 — base-16 'FF' → 255 (upper case via `*s & 0x1f`)"
);
assert_eq!(
zstrtol_underscore("DEADbeef", 16, false).0,
0xDEADBEEF,
"c:2485 — mixed case 32-bit hex"
);
}
#[test]
fn zstrtol_underscore_underscore_flag() {
let _g = crate::test_util::global_state_lock();
let (v, rest) = zstrtol_underscore("1_000", 10, false);
assert_eq!(v, 1, "c:2467 — underscore=false: `_` terminates");
assert_eq!(rest, "_000");
let (v, rest) = zstrtol_underscore("1_000_000", 10, true);
assert_eq!(
v, 1_000_000,
"c:2469-2470 — underscore=true: `_` skipped during accumulation"
);
assert_eq!(rest, "", "fully consumed including `_`s");
}
#[test]
fn timespec_diff_us_sign_matches_c_t2_minus_t1() {
let _g = crate::test_util::global_state_lock();
let t1 = std::time::Instant::now();
std::thread::sleep(std::time::Duration::from_millis(2));
let t2 = std::time::Instant::now();
let d = timespec_diff_us(&t1, &t2);
assert!(d > 0, "c:2759 — t2 after t1 → positive delta");
assert!(d >= 1000, "expected >= 1ms (1000us); got {}us", d);
let r = timespec_diff_us(&t2, &t1);
assert!(r < 0, "c:2770 — t1 after t2 → negative delta");
assert_eq!(d, -r, "swap of args negates the result");
}
#[test]
fn timespec_diff_us_same_instant_returns_zero() {
let _g = crate::test_util::global_state_lock();
let t = std::time::Instant::now();
assert_eq!(
timespec_diff_us(&t, &t),
0,
"c:2752 — identical Instants → 0"
);
}
#[test]
fn ucs4toutf8_encodes_canonical_lengths() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
ucs4toutf8(0x41),
Some("A".to_string()),
"c:6750 — 0x41 → 'A' (1 byte)"
);
assert_eq!(
ucs4toutf8(0xe9),
Some("é".to_string()),
"c:6752 — U+00E9 → 'é' (2 bytes)"
);
assert_eq!(
ucs4toutf8(0x5B57),
Some("字".to_string()),
"c:6754 — U+5B57 → '字' (3 bytes)"
);
assert_eq!(
ucs4toutf8(0x1D11E),
Some("𝄞".to_string()),
"c:6756 — U+1D11E → '𝄞' (4 bytes)"
);
}
#[test]
fn ucs4toutf8_rejects_invalid_codepoints() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
ucs4toutf8(0xFFFF_FFFE),
None,
"c:6763 — values >= 0x80000000 → None"
);
assert_eq!(
ucs4toutf8(0x8000_0000),
None,
"c:6760-6764 — exactly 0x80000000 is out of range"
);
assert!(
ucs4toutf8(0xD800).is_some(),
"c:6754 — surrogates encode as 3-byte UTF-8 in C (no Unicode validity check)"
);
}
#[test]
fn dquotedztrdup_default_path_wraps_whole_string() {
let _g = crate::test_util::global_state_lock();
if isset(CSHJUNKIEQUOTES) {
return; }
assert_eq!(
dquotedztrdup("hello"),
"\"hello\"",
"c:6690+6711+6718 — wrap plain text in double quotes"
);
assert_eq!(
dquotedztrdup("$var"),
"\"\\$var\"",
"c:6703-6708 — `$` → `\\$`"
);
assert_eq!(
dquotedztrdup("a\"b"),
"\"a\\\"b\"",
"c:6703-6708 — `\"` → `\\\"`"
);
}
#[test]
fn dquotedztrdup_pending_backslash_only_doubles_when_needed() {
let _g = crate::test_util::global_state_lock();
if isset(CSHJUNKIEQUOTES) {
return;
}
assert_eq!(
dquotedztrdup("a\\b"),
"\"a\\b\"",
"c:6711+6712 — `\\b` mid-string: no extra (pending=0 after b)"
);
let r = dquotedztrdup("a\\");
assert!(
r.ends_with("\\\\\""),
"c:6716-6717 — trailing `\\` → emit extra `\\` before closing `\"`: got {:?}",
r
);
}
#[test]
fn quotedzputs_empty_string_returns_double_quotes() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotedzputs(""), "''", "c:6470-6475 — empty input → ''");
}
#[test]
fn quotedzputs_wraps_specials_in_single_quotes() {
let _g = crate::test_util::global_state_lock();
inittyptab();
assert_eq!(quotedzputs("hello world"), "'hello world'");
assert_eq!(quotedzputs("foo=bar"), "'foo=bar'");
assert_eq!(
quotedzputs("it's"),
"'it'\\''s'",
"c:6517-6519 — apostrophe → '\\''"
);
}
#[test]
fn quotedzputs_plain_alnum_returns_unchanged() {
let _g = crate::test_util::global_state_lock();
inittyptab();
assert_eq!(
quotedzputs("hello"),
"hello",
"c:6512 — no specials, no wrap"
);
assert_eq!(quotedzputs("abc123"), "abc123");
}
#[test]
fn quotedzputs_avoids_empty_quoted_runs_on_boundaries() {
let _g = crate::test_util::global_state_lock();
inittyptab();
assert_eq!(
quotedzputs("'x"),
"\\''x'",
"c:6587-6610 — leading `'` produces `\\'` + opening `'`"
);
assert_eq!(
quotedzputs("x'"),
"'x'\\'",
"c:6631-6637 — trailing `'` doesn't generate empty `''`"
);
}
#[test]
fn quotedzputs_dollar_quote_escapes_apostrophe_inside_wrap() {
let _g = crate::test_util::global_state_lock();
inittyptab();
let r = quotedzputs("a\nb'c");
assert!(
r.starts_with("$'") && r.ends_with("'"),
"c:6488 — must be wrapped in $'...' got {:?}",
r
);
assert!(
r.contains("\\'"),
"c:5413-5414 — embedded `'` must be `\\'` not raw `'`: got {:?}",
r
);
}
#[test]
fn quotedzputs_rcquotes_doubles_apostrophe() {
let _g = crate::test_util::global_state_lock();
inittyptab();
let prev = crate::ported::options::opt_state_get("rcquotes").unwrap_or(false);
crate::ported::options::opt_state_set("rcquotes", true);
let got = quotedzputs("it's");
crate::ported::options::opt_state_set("rcquotes", prev);
assert_eq!(
got, "'it''s'",
"c:6548-6553 — RCQUOTES: `'` → `''` (doubled)"
);
}
#[test]
fn strucpy_appends_t_to_dest() {
let _g = crate::test_util::global_state_lock();
let mut dest = String::from("prefix-");
strucpy(&mut dest, "suffix");
assert_eq!(
dest, "prefix-suffix",
"c:2335 — strucpy appends t (NOT case-changes — c-name 'u' is pointer-walk, not upper)"
);
let mut dest = String::from("alone");
strucpy(&mut dest, "");
assert_eq!(dest, "alone");
let mut dest = String::new();
strucpy(&mut dest, "hello");
assert_eq!(dest, "hello");
}
#[test]
fn struncpy_appends_up_to_n_bytes() {
let _g = crate::test_util::global_state_lock();
let mut dest = String::from("X");
struncpy(&mut dest, "abcdef", 3);
assert_eq!(dest, "Xabc", "c:2345 — clipped to n=3 bytes of `abcdef`");
let mut dest = String::from("Y");
struncpy(&mut dest, "abc", 100);
assert_eq!(dest, "Yabc", "c:2345 — full copy when n >= len");
let mut dest = String::from("Z");
struncpy(&mut dest, "abc", 0);
assert_eq!(dest, "Z", "c:2345 — n=0 stops the copy loop immediately");
}
#[test]
fn has_token_detects_typtab_token_bytes() {
let _g = crate::test_util::global_state_lock();
inittyptab();
assert!(!has_token(""), "empty: no tokens");
assert!(
!has_token("hello world"),
"c:2285 — ASCII text has no token bytes"
);
let s: String = std::iter::once(0x84u8 as char).collect();
assert!(
has_token(&s),
"c:2285 — Pound (0x84) is itok → has_token=true"
);
let s: String = std::iter::once(0x9cu8 as char).collect();
assert!(has_token(&s), "c:2285 — Bang (0x9c) is itok");
let s: String = std::iter::once(0xa1u8 as char).collect();
assert!(has_token(&s), "c:2285 — Nularg (0xa1) is itok");
}
#[test]
fn has_token_excludes_meta_byte() {
let _g = crate::test_util::global_state_lock();
inittyptab();
let s: String = std::iter::once(0x83u8 as char).collect();
assert!(
!has_token(&s),
"c:2285 — Meta (0x83) is NOT itok; previous hardcoded 0x83 list misfired"
);
}
#[test]
fn addunprintable_named_control_escapes() {
let _g = crate::test_util::global_state_lock();
assert_eq!(addunprintable('\x07'), "\\a", "c:6106 — BEL → \\a");
assert_eq!(addunprintable('\x08'), "\\b", "c:6107 — BS → \\b");
assert_eq!(addunprintable('\x0c'), "\\f", "c:6108 — FF → \\f");
assert_eq!(addunprintable('\n'), "\\n", "c:6109 — LF → \\n");
assert_eq!(addunprintable('\r'), "\\r", "c:6110 — CR → \\r");
assert_eq!(addunprintable('\t'), "\\t", "c:6111 — TAB → \\t");
assert_eq!(addunprintable('\x0b'), "\\v", "c:6112 — VT → \\v");
}
#[test]
fn addunprintable_nul_renders_as_backslash_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(addunprintable('\0'), "\\0", "c:6097-6098 — NUL → \\0");
}
#[test]
fn addunprintable_octal_fallback_for_unnamed_controls() {
let _g = crate::test_util::global_state_lock();
assert_eq!(addunprintable('\x01'), "\\001", "c:6116-6118 — SOH → \\001");
assert_eq!(addunprintable('\x1b'), "\\033", "c:6116-6118 — ESC → \\033");
assert_eq!(addunprintable('\x7f'), "\\177", "c:6116-6118 — DEL → \\177");
assert_eq!(
addunprintable(char::from_u32(0xff).unwrap()),
"\\377",
"c:6116-6118 — 0xff → \\377"
);
}
#[test]
fn hasspecial_recognises_canonical_special_chars() {
let _g = crate::test_util::global_state_lock();
inittyptab();
for c in "#$^*()=|{}[]<>?~;&".chars() {
let s = c.to_string();
assert!(
hasspecial(&s),
"c:6075 — '{}' is in SPECCHARS, must be special",
c
);
}
assert!(hasspecial(" "), "space in SPECCHARS");
assert!(hasspecial("\t"), "tab in SPECCHARS");
assert!(hasspecial("\n"), "newline in SPECCHARS");
assert!(
!hasspecial("hello"),
"c:6075 — plain alphanumerics are NOT special"
);
assert!(!hasspecial("ABC012"));
}
#[test]
fn hasspecial_decodes_meta_byte_before_check() {
let _g = crate::test_util::global_state_lock();
inittyptab();
let bytes: Vec<u8> = vec![Meta, 0x41u8];
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
assert!(
!hasspecial(s),
"c:6075 — Meta+0x41 decodes to 'a' which is NOT special"
);
}
#[test]
fn sb_niceformat_passes_printable_ascii() {
let _g = crate::test_util::global_state_lock();
let mut out: Option<String> = None;
let l = sb_niceformat("hello", None, Some(&mut out), 0);
assert_eq!(
out.as_deref(),
Some("hello"),
"c:5886 — nicechar_sel passes printable through"
);
assert_eq!(l, 5, "c:5928 — return length equals output bytes");
let mut out: Option<String> = None;
sb_niceformat("", None, Some(&mut out), 0);
assert_eq!(out.as_deref(), Some(""));
let mut out: Option<String> = None;
sb_niceformat("ABC012!?@", None, Some(&mut out), 0);
assert_eq!(out.as_deref(), Some("ABC012!?@"));
}
#[test]
fn sb_niceformat_escapes_controls() {
let _g = crate::test_util::global_state_lock();
let mut out: Option<String> = None;
sb_niceformat("a\nb", None, Some(&mut out), 0);
assert_eq!(out.as_deref(), Some("a\\nb"), "c:5886 — newline → \\n");
let mut out: Option<String> = None;
sb_niceformat("a\tb", None, Some(&mut out), 0);
assert_eq!(out.as_deref(), Some("a\\tb"));
let mut out: Option<String> = None;
sb_niceformat("\x01", None, Some(&mut out), 0);
assert_eq!(
out.as_deref(),
Some("^A"),
"c:5886 — control char → ^X form"
);
let mut out: Option<String> = None;
sb_niceformat("\x7f", None, Some(&mut out), 0);
assert_eq!(out.as_deref(), Some("^?"));
}
#[test]
fn sb_niceformat_unmetafies_before_formatting() {
let _g = crate::test_util::global_state_lock();
let bytes: Vec<u8> = vec![Meta, 0x41u8];
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
let mut out: Option<String> = None;
sb_niceformat(s, None, Some(&mut out), 0);
assert_eq!(
out.as_deref(),
Some("a"),
"c:5872 — unmetafy first: Meta+0x41 → 'a' → printable passthrough"
);
let bytes: Vec<u8> = vec![Meta, 0x20u8];
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
let mut out: Option<String> = None;
sb_niceformat(s, None, Some(&mut out), 0);
let r = out.unwrap_or_default();
assert!(
!r.is_empty(),
"c:5872 — Meta+0x20 → NUL → must emit some escape, not empty"
);
}
#[test]
fn is_sb_niceformat_true_for_strings_with_controls() {
let _g = crate::test_util::global_state_lock();
assert_eq!(is_sb_niceformat("\n"), 1, "newline is nice");
assert_eq!(is_sb_niceformat("a\tb"), 1);
assert_eq!(is_sb_niceformat("\x01"), 1);
assert_eq!(is_sb_niceformat("hello"), 0);
assert_eq!(is_sb_niceformat(""), 0);
}
#[test]
fn metacharlenconv_plain_ascii_returns_one_byte() {
let _g = crate::test_util::global_state_lock();
let (n, c) = metacharlenconv("a");
assert_eq!((n, c), (1, Some('a')), "c:5823 — plain byte → (1, byte)");
let (n, c) = metacharlenconv("X");
assert_eq!((n, c), (1, Some('X')));
}
#[test]
fn metacharlenconv_meta_pair_xor_decodes() {
let _g = crate::test_util::global_state_lock();
let bytes: Vec<u8> = vec![Meta, 0x41u8];
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
let (n, c) = metacharlenconv(s);
assert_eq!(
(n, c),
(2, Some('a')),
"c:5820 — Meta+0x41 → 'a' (XOR 32), consumed 2 bytes"
);
let (n, c) = metacharlenconv("");
assert_eq!((n, c), (0, None));
}
#[test]
fn charlenconv_zero_len_returns_zero() {
let _g = crate::test_util::global_state_lock();
let (n, c) = charlenconv("abc", 0);
assert_eq!((n, c), (0, None), "c:5834-5837 — len=0 returns 0");
}
#[test]
fn charlenconv_returns_first_byte_for_nonzero_len() {
let _g = crate::test_util::global_state_lock();
let (n, c) = charlenconv("abc", 3);
assert_eq!((n, c), (1, Some('a')), "c:5841 — *c = *x; return 1");
let (n, c) = charlenconv("xy", 2);
assert_eq!((n, c), (1, Some('x')));
}
#[test]
fn is_mb_niceformat_false_for_pure_printable_ascii() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
is_mb_niceformat("hello"),
0,
"c:5509 — printable ASCII needs no nice-format"
);
assert_eq!(
is_mb_niceformat(""),
0,
"c:5486 — empty string has no chars to flag"
);
assert_eq!(is_mb_niceformat("ABC012!?@"), 0);
}
#[test]
fn is_mb_niceformat_true_for_strings_with_controls() {
let _g = crate::test_util::global_state_lock();
assert_eq!(is_mb_niceformat("a\nb"), 1, "c:5509 — newline is nice");
assert_eq!(is_mb_niceformat("\t"), 1);
assert_eq!(is_mb_niceformat("\x01"), 1, "c:5509 — control char is nice");
assert_eq!(is_mb_niceformat("\x7f"), 1, "c:5509 — DEL is nice");
}
#[test]
fn mb_niceformat_preserves_printable_wide_chars() {
let _g = crate::test_util::global_state_lock();
let mut out: Option<String> = None;
mb_niceformat("hello", None, Some(&mut out), 0);
assert_eq!(
out.as_deref(),
Some("hello"),
"c:5407 — printable ASCII passes through"
);
let mut out: Option<String> = None;
mb_niceformat("café", None, Some(&mut out), 0);
assert_eq!(
out.as_deref(),
Some("café"),
"c:5407 — Latin-1 'é' must NOT byte-mask to \\M-X"
);
let mut out: Option<String> = None;
mb_niceformat("字", None, Some(&mut out), 0);
assert_eq!(
out.as_deref(),
Some("字"),
"c:5407 — CJK printable passes through"
);
let mut out: Option<String> = None;
mb_niceformat("abcéxyz", None, Some(&mut out), 0);
assert_eq!(out.as_deref(), Some("abcéxyz"));
}
#[test]
fn mb_niceformat_escapes_controls() {
let _g = crate::test_util::global_state_lock();
let mut out: Option<String> = None;
mb_niceformat("\n", None, Some(&mut out), 0);
assert_eq!(
out.as_deref(),
Some("\\n"),
"c:5407 → wcs_nicechar c:625 — newline escapes"
);
let mut out: Option<String> = None;
mb_niceformat("\t", None, Some(&mut out), 0);
assert_eq!(
out.as_deref(),
Some("\\t"),
"c:5407 → wcs_nicechar c:628 — tab escapes"
);
let mut out: Option<String> = None;
mb_niceformat("a\nb", None, Some(&mut out), 0);
assert_eq!(out.as_deref(), Some("a\\nb"));
}
#[test]
fn metalen_returns_metafied_byte_count() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
metalen("hello", 5),
5,
"c:4972 — ASCII: metafied bytes == unmetafied chars"
);
let bytes: Vec<u8> = vec![b'a', Meta, 0x41, b'b'];
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
assert_eq!(
metalen(s, 3),
4,
"c:4978 — 3 unmetafied chars + 1 Meta = 4 metafied bytes"
);
let bytes: Vec<u8> = vec![Meta, 0x41, Meta, 0x42];
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
assert_eq!(
metalen(s, 2),
4,
"c:4978 — 2 unmetafied chars (both Meta+X) = 4 metafied bytes"
);
}
#[test]
fn metalen_returns_input_for_no_meta_chars() {
let _g = crate::test_util::global_state_lock();
assert_eq!(metalen("", 0), 0, "c:4974 — empty input returns 0");
assert_eq!(
metalen("abc", 3),
3,
"c:4974 — no Meta chars: output == input len"
);
}
#[test]
fn unmeta_one_empty_input_returns_zero() {
let _g = crate::test_util::global_state_lock();
let (c, n) = unmeta_one("");
assert_eq!(c, '\0', "c:5070 — empty input returns NUL char");
assert_eq!(n, 0, "c:5070 — empty input consumes 0 bytes");
}
#[test]
fn unmeta_one_plain_ascii_consumes_one_byte() {
let _g = crate::test_util::global_state_lock();
for c in "aA0!~".chars() {
let s = c.to_string();
let (got, n) = unmeta_one(&s);
assert_eq!(got, c, "c:5082 — '{}' decodes to itself", c);
assert_eq!(n, 1, "c:5081 — non-Meta byte consumes 1");
}
}
#[test]
fn unmeta_one_meta_pair_decodes_xor_32() {
let _g = crate::test_util::global_state_lock();
let bytes: Vec<u8> = vec![Meta, 0x41u8];
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
let (got, n) = unmeta_one(s);
assert_eq!(got, 'a', "c:5079 — Meta+0x41 decodes to 0x41^32 = 'a'");
assert_eq!(n, 2, "c:5078 — Meta pair consumes 2 bytes");
let bytes: Vec<u8> = vec![Meta, 0x20u8];
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
let (got, n) = unmeta_one(s);
assert_eq!(
got, '\0',
"c:5079 — Meta+0x20 decodes to NUL (the canonical metafy-NUL pattern)"
);
assert_eq!(n, 2);
}
#[test]
fn unmeta_one_trailing_meta_byte_falls_through() {
let _g = crate::test_util::global_state_lock();
let bytes: Vec<u8> = vec![Meta];
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
let (_, n) = unmeta_one(s);
assert_eq!(n, 1, "trailing Meta — no panic, no read past buffer");
}
#[test]
fn ztrsub_ascii_no_meta_returns_byte_distance() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
ztrsub("hello world", 0, 5),
5,
"c:5189 — no Meta: returns t-s byte distance"
);
assert_eq!(ztrsub("hello world", 6, 11), 5);
assert_eq!(ztrsub("hello", 2, 2), 0);
}
#[test]
fn ztrsub_subtracts_one_per_meta_pair() {
let _g = crate::test_util::global_state_lock();
let meta_byte = Meta;
let buf_bytes: Vec<u8> = vec![b'a', meta_byte, 0x20, b'b'];
let buf = unsafe { std::str::from_utf8_unchecked(&buf_bytes) };
assert_eq!(
ztrsub(buf, 0, 4),
3,
"c:5192-5199 — Meta pair decrements distance by 1"
);
}
#[test]
fn ztrsub_clamps_inverted_range_to_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
ztrsub("hello", 4, 2),
0,
"inverted range start>end → 0 (defensive; not in C contract)"
);
assert_eq!(
ztrsub("hi", 0, 100),
2,
"end > buf.len() clamps to buffer length"
);
}
#[test]
fn ztrlen_handles_trailing_meta_byte_without_panic() {
let _g = crate::test_util::global_state_lock();
let meta = char::from_u32(Meta as u32).unwrap();
let trailing: String = ['a', meta].iter().collect();
let r = ztrlen(&trailing);
assert!(r >= 1, "trailing Meta must not panic; got {}", r);
}
#[test]
fn isident_rejects_digit_leading_names() {
let _g = crate::test_util::global_state_lock(); assert!(!isident("1foo")); assert!(
isident("99"), "c:1315-1319 — all-digit names are valid positional params"
);
assert!(!isident("")); }
#[test]
fn isident_accepts_underscore_and_alpha_leading() {
let _g = crate::test_util::global_state_lock(); assert!(isident("foo")); assert!(isident("_foo")); assert!(isident("Foo_BAR_42")); assert!(isident("a")); }
#[test]
fn isident_rejects_special_chars() {
let _g = crate::test_util::global_state_lock(); assert!(!isident("foo bar")); assert!(!isident("foo-bar")); assert!(!isident("a$b")); }
#[test]
fn convbase_zero_renders_as_zero_literal() {
let _g = crate::test_util::global_state_lock();
assert_eq!(convbase(0, 10), "0");
}
#[test]
fn convbase_uses_base_prefix_syntax_for_non_decimal() {
let _g = crate::test_util::global_state_lock();
assert_eq!(convbase(255, 16), "16#FF");
assert_eq!(convbase(8, 8), "8#10");
assert_eq!(convbase(5, 2), "2#101");
assert_eq!(convbase(42, 10), "42");
}
#[test]
fn convbase_preserves_negative_sign() {
let _g = crate::test_util::global_state_lock();
assert_eq!(convbase(-42, 10), "-42");
}
#[test]
fn slashsplit_keeps_leading_empty_segment() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
slashsplit("/usr/local/bin"),
vec![
"".to_string(),
"usr".to_string(),
"local".to_string(),
"bin".to_string()
],
"c:851 — leading `/` produces empty first segment"
);
assert_eq!(
slashsplit("/"),
vec!["".to_string()],
"c:854-857 — trailing `/` after first iter returns ['']"
);
assert_eq!(
slashsplit("//foo"),
vec!["".to_string(), "foo".to_string()],
"c:852-853 — consecutive `/` collapse, leading empty kept"
);
assert_eq!(
slashsplit(""),
Vec::<String>::new(),
"c:842 — empty input → empty array"
);
}
#[test]
fn slashsplit_relative_path_no_trailing_empty() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
slashsplit("a/b/"),
vec!["a".to_string(), "b".to_string()],
"c:854-857 — trailing `/` doesn't add segment"
);
assert_eq!(
slashsplit("a//b"),
vec!["a".to_string(), "b".to_string()],
"c:852-853 — mid `//` collapses"
);
}
#[test]
fn equalsplit_returns_first_equals_split() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
equalsplit("foo=bar"),
Some(("foo".to_string(), "bar".to_string()))
);
assert_eq!(
equalsplit("a=b=c"),
Some(("a".to_string(), "b=c".to_string())),
"splits on FIRST `=` only"
);
}
#[test]
fn equalsplit_no_equals_returns_none() {
let _g = crate::test_util::global_state_lock();
assert_eq!(equalsplit("foo"), None);
assert_eq!(equalsplit(""), None);
}
#[test]
fn ztrcmp_deterministic_and_lexicographic() {
let _g = crate::test_util::global_state_lock();
assert_eq!(ztrcmp("abc", "abc"), std::cmp::Ordering::Equal);
assert_eq!(ztrcmp("abc", "abd"), std::cmp::Ordering::Less);
assert_eq!(ztrcmp("abd", "abc"), std::cmp::Ordering::Greater);
assert_eq!(ztrcmp("", ""), std::cmp::Ordering::Equal);
assert_eq!(ztrcmp("", "a"), std::cmp::Ordering::Less);
}
#[test]
fn ztrcmp_shorter_prefix_is_less() {
let _g = crate::test_util::global_state_lock();
assert_eq!(ztrcmp("a", "ab"), std::cmp::Ordering::Less);
assert_eq!(ztrcmp("foo", "foob"), std::cmp::Ordering::Less);
}
#[test]
fn ztrcmp_decodes_meta_byte_for_comparison() {
let _g = crate::test_util::global_state_lock();
let meta_byte = Meta;
let s1_bytes: Vec<u8> = vec![meta_byte, 0x41u8]; let s2_bytes: Vec<u8> = vec![b'b'];
let s1 = unsafe { std::str::from_utf8_unchecked(&s1_bytes) };
let s2 = unsafe { std::str::from_utf8_unchecked(&s2_bytes) };
assert_eq!(
ztrcmp(s1, s2),
std::cmp::Ordering::Less,
"c:5117-5118 — Meta+0x41 decodes to 'a' which < 'b'"
);
}
#[test]
fn is_nicechar_printable_ascii_is_not_nice() {
let _g = crate::test_util::global_state_lock();
for c in "abcXYZ012!?@~".chars() {
assert!(
!is_nicechar(c),
"c:534 — '{}' (ASCII printable) must NOT be nice",
c
);
}
assert!(!is_nicechar(' '));
}
#[test]
fn is_nicechar_control_chars_are_nice() {
let _g = crate::test_util::global_state_lock();
assert!(is_nicechar('\n'), "c:538 — newline is nice");
assert!(is_nicechar('\t'), "c:538 — tab is nice");
assert!(is_nicechar('\x7f'), "c:538 — DEL is nice");
assert!(is_nicechar('\x00'), "c:538 — NUL is nice (<0x20)");
assert!(is_nicechar('\x07'), "c:538 — BEL is nice (<0x20)");
assert!(is_nicechar('\x1b'), "c:538 — ESC is nice (<0x20)");
assert!(is_nicechar('\x1f'), "c:538 — boundary 0x1f is nice");
}
#[test]
fn zstrtoul_underscore_recognises_hex_binary_decimal() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zstrtoul_underscore("0xff"), Some(255));
assert_eq!(zstrtoul_underscore("0XFF"), Some(255));
assert_eq!(zstrtoul_underscore("0b1010"), Some(10));
assert_eq!(zstrtoul_underscore("0B11"), Some(3));
assert_eq!(zstrtoul_underscore("12345"), Some(12345));
if !isset(OCTALZEROES) {
assert_eq!(
zstrtoul_underscore("0777"),
Some(777),
"c:2543 — OCTALZEROES off: leading-0 parses as decimal"
);
assert_eq!(zstrtoul_underscore("010"), Some(10));
}
}
#[test]
fn zstrtoul_underscore_strips_underscores() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zstrtoul_underscore("1_000_000"),
Some(1_000_000),
"c:2547-2548 — `_` stripped from numeric input"
);
assert_eq!(zstrtoul_underscore("0xff_ff"), Some(0xffff));
assert_eq!(zstrtoul_underscore("0b1010_1010"), Some(0xaa));
}
#[test]
fn zstrtoul_underscore_consumes_leading_plus() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zstrtoul_underscore("+42"), Some(42));
assert_eq!(zstrtoul_underscore("+0xff"), Some(255));
}
#[test]
fn nicechar_sel_passes_printable_ascii_unchanged() {
let _g = crate::test_util::global_state_lock();
for c in "aA0!~".chars() {
assert_eq!(
nicechar_sel(c, false),
c.to_string(),
"c:467 — printable ASCII '{}' passes through",
c
);
}
}
#[test]
fn nicechar_sel_escapes_newline_and_tab() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
nicechar_sel('\n', false),
"\\n",
"c:487-489 — newline escape"
);
assert_eq!(nicechar_sel('\t', false), "\\t", "c:490-492 — tab escape");
}
#[test]
fn nicechar_sel_control_chars_use_caret_or_c_prefix() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
nicechar_sel('\x01', false),
"^A",
"c:499-500 — \\x01 → ^A non-quotable"
);
assert_eq!(
nicechar_sel('\x01', true),
"\\C-A",
"c:495-500 — \\x01 → \\C-A quotable"
);
assert_eq!(nicechar_sel('\x07', false), "^G");
assert_eq!(nicechar_sel('\x1b', false), "^[");
}
#[test]
fn nicechar_sel_del_renders_as_caret_question() {
let _g = crate::test_util::global_state_lock();
assert_eq!(nicechar_sel('\x7f', false), "^?", "c:485-486 — DEL is `^?`");
assert_eq!(
nicechar_sel('\x7f', true),
"\\C-?",
"c:481-486 — DEL is `\\C-?` quotable"
);
}
#[test]
fn nicechar_sel_highbit_uses_meta_prefix_when_printeightbit_off() {
let _g = crate::test_util::global_state_lock();
if isset(PRINTEIGHTBIT) {
return; }
let c = char::from_u32(0xc1).unwrap();
assert_eq!(
nicechar_sel(c, false),
"\\M-A",
"c:472-477 — high-bit 0xc1 → \\M-A under PRINTEIGHTBIT-off"
);
let c = char::from_u32(0xe1).unwrap();
assert_eq!(nicechar_sel(c, false), "\\M-a");
}
#[test]
fn wcs_nicechar_sel_printable_wide_emits_utf8() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
wcs_nicechar_sel('a', None, None, false),
"a",
"ASCII printable"
);
assert_eq!(
wcs_nicechar_sel('é', None, None, false),
"é",
"Latin-1 printable"
);
assert_eq!(
wcs_nicechar_sel('字', None, None, false),
"字",
"CJK printable"
);
}
#[test]
fn wcs_nicechar_sel_escapes_newline_and_tab() {
let _g = crate::test_util::global_state_lock();
assert_eq!(wcs_nicechar_sel('\n', None, None, false), "\\n");
assert_eq!(wcs_nicechar_sel('\t', None, None, false), "\\t");
}
#[test]
fn wcs_nicechar_sel_del_uses_caret_question() {
let _g = crate::test_util::global_state_lock();
assert_eq!(wcs_nicechar_sel('\x7f', None, None, false), "^?");
assert_eq!(wcs_nicechar_sel('\x7f', None, None, true), "\\C-?");
}
#[test]
fn wcs_nicechar_sel_control_chars_use_caret_prefix() {
let _g = crate::test_util::global_state_lock();
assert_eq!(wcs_nicechar_sel('\x01', None, None, false), "^A");
assert_eq!(wcs_nicechar_sel('\x07', None, None, false), "^G");
assert_eq!(wcs_nicechar_sel('\x1b', None, None, false), "^[");
assert_eq!(wcs_nicechar_sel('\x01', None, None, true), "\\C-A");
}
#[test]
fn wcs_nicechar_sel_large_nonprintable_uses_hex_escape() {
let _g = crate::test_util::global_state_lock();
let nel = char::from_u32(0x85).unwrap();
let r = wcs_nicechar_sel(nel, None, None, false);
assert!(!r.is_empty(), "must emit something for U+0085");
let zwsp = char::from_u32(0x200B).unwrap();
let r = wcs_nicechar_sel(zwsp, None, None, false);
assert!(!r.is_empty());
}
#[test]
fn wcs_nicechar_matches_wcs_nicechar_sel_with_zero() {
let _g = crate::test_util::global_state_lock();
for c in ['a', '\n', '\t', '\x7f', '\x01', 'é', '字'] {
assert_eq!(
wcs_nicechar(c, None, None),
wcs_nicechar_sel(c, None, None, false),
"c:711 — `wcs_nicechar(c, NULL, NULL)` must equal `wcs_nicechar_sel(c, NULL, NULL, 0)` for {:?}",
c
);
}
}
#[test]
#[cfg(unix)]
fn movefd_returns_minus_one_unchanged() {
let _g = crate::test_util::global_state_lock();
assert_eq!(movefd(-1), -1, "fd=-1 must be returned unchanged");
}
#[test]
#[cfg(unix)]
fn movefd_returns_high_fd_unchanged() {
let _g = crate::test_util::global_state_lock();
let f = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
if f < 0 {
return;
} let high = unsafe { libc::fcntl(f, libc::F_DUPFD, 20) };
unsafe {
libc::close(f);
}
if high < 0 {
return;
}
let r = movefd(high);
assert_eq!(r, high, "c:1992 — fd >= 10 returned unchanged");
unsafe {
libc::close(high);
}
}
#[test]
fn check_fd_table_grows_to_fd_plus_one() {
let _g = crate::test_util::global_state_lock();
MAX_ZSH_FD.store(-1, Ordering::Relaxed);
{
let mut g = fdtable_lock().lock().unwrap();
g.clear();
}
assert!(check_fd_table(-1));
assert_eq!(
MAX_ZSH_FD.load(Ordering::Relaxed),
-1,
"fd ≤ cur_max early-returns; MAX_ZSH_FD untouched"
);
assert!(check_fd_table(7));
assert_eq!(
MAX_ZSH_FD.load(Ordering::Relaxed),
7,
"c:1982 — MAX_ZSH_FD := fd"
);
let len = {
let g = fdtable_lock().lock().unwrap();
g.len()
};
assert!(
len >= 8,
"c:1975-1979 — fdtable grew to ≥ fd+1 = 8 slots, got {}",
len
);
assert!(check_fd_table(3));
assert_eq!(
MAX_ZSH_FD.load(Ordering::Relaxed),
7,
"fd ≤ cur_max keeps MAX_ZSH_FD at 7"
);
}
#[test]
fn wcsitype_iword_reads_from_canonical_wordchars_global() {
let _g = crate::test_util::global_state_lock();
if !isset(MULTIBYTE) {
return;
}
let saved = crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| {
t.get("WORDCHARS")
.map(|pm| crate::ported::params::wordcharsgetfn(pm))
})
.unwrap_or_default();
let mut dummy = crate::ported::zsh_h::param::default();
let do_set = |dummy: &mut crate::ported::zsh_h::param, val: String| {
wordcharssetfn(dummy, val);
};
do_set(&mut dummy, "é".to_string());
do_set(&mut dummy, ":".to_string());
assert!(
wcsitype(':', IWORD as u32),
"c:4364 — wordchars membership through canonical global"
);
do_set(&mut dummy, saved);
}
#[cfg(unix)]
#[test]
fn addmodulefd_respects_fdt_parameter() {
let _g = crate::test_util::global_state_lock();
let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
if fd < 0 {
return;
}
addmodulefd(fd, FDT_EXTERNAL);
assert_eq!(
fdtable_get(fd),
FDT_EXTERNAL,
"c:2095 — fdt parameter must be stored verbatim"
);
addmodulefd(fd, FDT_MODULE);
assert_eq!(
fdtable_get(fd),
FDT_MODULE,
"c:2095 — second call overwrites with new fdt"
);
unsafe {
libc::close(fd);
}
}
#[test]
fn addmodulefd_ignores_negative_fd() {
let _g = crate::test_util::global_state_lock();
addmodulefd(-1, FDT_MODULE);
}
#[cfg(unix)]
#[test]
fn zcloselockfd_returns_minus_one_for_non_lock_fd() {
let _g = crate::test_util::global_state_lock();
let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
if fd < 0 {
return;
}
let r = zcloselockfd(fd);
assert_eq!(r, -1, "c:2160-2161 — non-lock fd must return -1, NOT 0");
unsafe {
libc::close(fd);
}
}
#[cfg(unix)]
#[test]
fn zcloselockfd_returns_zero_for_flock_fd_then_closes() {
let _g = crate::test_util::global_state_lock();
let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
if fd < 0 {
return;
}
addlockfd(fd, true);
let r = zcloselockfd(fd);
assert_eq!(r, 0, "c:2162-2163 — FDT_FLOCK fd → zclose + return 0");
}
#[cfg(unix)]
#[test]
fn fdtable_set_bumps_max_zsh_fd() {
let _g = crate::test_util::global_state_lock();
let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
if fd < 0 {
return;
}
fdtable_set(fd, FDT_FLOCK);
let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
assert!(
max_fd >= fd,
"c:1982 — max_zsh_fd ({}) must be >= newly-set fd ({})",
max_fd,
fd
);
fdtable_set(fd, FDT_UNUSED);
unsafe {
libc::close(fd);
}
}
#[cfg(unix)]
#[test]
fn addlockfd_selects_flock_category_per_cloexec_flag() {
let _g = crate::test_util::global_state_lock();
let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
if fd < 0 {
return;
}
addlockfd(fd, true);
assert_eq!(
fdtable_get(fd),
FDT_FLOCK,
"c:2117 — cloexec=true → FDT_FLOCK"
);
addlockfd(fd, false);
assert_eq!(
fdtable_get(fd),
FDT_FLOCK_EXEC,
"c:2119 — cloexec=false → FDT_FLOCK_EXEC"
);
unsafe {
libc::close(fd);
}
}
#[cfg(unix)]
#[test]
fn adduserdir_rejects_paths_at_or_above_path_max() {
let _g = crate::test_util::global_state_lock();
if !interact() {
return;
}
let name = "ZSHRS_TEST_PATHMAX_DIR";
let _ = removenameddirnode(name);
let mut over: String = "/".to_string();
over.push_str(&"a".repeat(libc::PATH_MAX as usize));
adduserdir(name, &over, 0, true);
let tab = nameddirtab().lock().unwrap();
assert!(
!tab.contains_key(name),
"c:1211 — strlen(t) >= PATH_MAX must NOT insert entry"
);
}
#[test]
#[cfg(unix)]
fn pathprog_finds_non_executable_files() {
let _g = crate::test_util::global_state_lock();
let test_name = format!("zshrs_test_pathprog_{}", unsafe { libc::getpid() });
let path = PathBuf::from("/tmp").join(&test_name);
if fs::write(&path, b"plain content").is_err() {
return; }
let meta = fs::metadata(&path).unwrap();
assert_eq!(
meta.permissions().mode() & 0o111,
0,
"test setup: must be non-executable"
);
let saved_path = getsparam("PATH");
assignsparam("PATH", "/tmp", 0);
let r = pathprog(&test_name);
let _ = fs::remove_file(&path);
if let Some(prev) = saved_path {
assignsparam("PATH", &prev, 0);
}
assert_eq!(
r,
Some(path),
"c:776 — pathprog finds non-executable files (only F_OK + !S_ISDIR)"
);
}
#[test]
#[cfg(unix)]
fn pathprog_skips_directories() {
let _g = crate::test_util::global_state_lock();
let test_name = format!("zshrs_test_pathprog_dir_{}", unsafe { libc::getpid() });
let path = PathBuf::from("/tmp").join(&test_name);
if fs::create_dir(&path).is_err() {
return;
}
let saved_path = getsparam("PATH");
assignsparam("PATH", "/tmp", 0);
let r = pathprog(&test_name);
let _ = fs::remove_dir(&path);
if let Some(prev) = saved_path {
assignsparam("PATH", &prev, 0);
}
assert!(
r.is_none(),
"c:778 — pathprog must skip directories (!S_ISDIR)"
);
}
#[test]
fn wcsitype_isep_reads_from_canonical_ifs_global() {
let _g = crate::test_util::global_state_lock();
if !isset(MULTIBYTE) {
return;
}
let saved = crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)))
.unwrap_or_default();
let mut dummy = crate::ported::zsh_h::param::default();
let do_set = |dummy: &mut crate::ported::zsh_h::param, val: String| {
ifssetfn(dummy, val);
};
do_set(&mut dummy, ":".to_string());
assert!(
wcsitype(':', ISEP as u32),
"c:4367 — IFS membership through canonical global"
);
do_set(&mut dummy, saved);
}
#[test]
fn is_nicechar_highbit_is_nice_when_printeightbit_off() {
let _g = crate::test_util::global_state_lock();
let c = char::from_u32(0xb5).unwrap();
if !isset(PRINTEIGHTBIT) {
assert!(
is_nicechar(c),
"c:536 — high-bit byte 0x{:x} must be nice when PRINTEIGHTBIT off",
0xb5_u32
);
}
}
#[test]
fn check_fd_table_grows_fdtable_and_bumps_max_zsh_fd() {
let _g = crate::test_util::global_state_lock();
let saved_max = MAX_ZSH_FD.load(Ordering::Relaxed);
let target = saved_max.max(50) + 7;
check_fd_table(target);
let new_max = MAX_ZSH_FD.load(Ordering::Relaxed);
assert!(
new_max >= target,
"c:1982 — max_zsh_fd must be >= target after grow (got {})",
new_max
);
check_fd_table(target - 3);
assert_eq!(
MAX_ZSH_FD.load(Ordering::Relaxed),
new_max,
"c:1971 — fd <= max_zsh_fd path must not change max"
);
MAX_ZSH_FD.store(saved_max, Ordering::Relaxed);
}
#[test]
fn check_fd_table_small_fd_is_noop() {
let _g = crate::test_util::global_state_lock();
let _ = check_fd_table(100);
let max_before = MAX_ZSH_FD.load(Ordering::Relaxed);
check_fd_table(5);
assert_eq!(
MAX_ZSH_FD.load(Ordering::Relaxed),
max_before,
"c:1971 — small fd path must not touch max_zsh_fd"
);
}
#[test]
fn check_fd_table_negative_fd_does_not_panic() {
let _g = crate::test_util::global_state_lock();
let _ = check_fd_table(-1);
let _ = check_fd_table(-100);
}
#[test]
fn nicezputs_matches_mb_niceformat_under_multibyte() {
let _g = crate::test_util::global_state_lock();
for input in ["hello", "a\nb", "é", ""] {
let mut nz_buf: Vec<u8> = Vec::new();
let _ = nicezputs(input, &mut nz_buf);
let nz = String::from_utf8(nz_buf).expect("utf8");
let mut mb_out: Option<String> = None;
let _ = mb_niceformat(input, None, Some(&mut mb_out), 0);
assert_eq!(
nz,
mb_out.unwrap_or_default(),
"nicezputs and mb_niceformat must agree for {:?}",
input
);
}
let mut nz_buf: Vec<u8> = Vec::new();
let _ = nicezputs("a\nb", &mut nz_buf);
let nz = String::from_utf8(nz_buf).expect("utf8");
assert!(nz.contains("\\n"), "nicechar emits `\\n`, not raw 0x0a");
}
#[test]
fn nicedup_matches_mb_niceformat_under_multibyte() {
let _g = crate::test_util::global_state_lock();
for input in ["hello", "a\nb", "é"] {
let nd = nicedup(input, 0);
let mut mb_out: Option<String> = None;
let _ = mb_niceformat(input, None, Some(&mut mb_out), 0);
assert_eq!(
nd,
mb_out.unwrap_or_default(),
"nicedup must equal mb_niceformat outstrp form for {:?}",
input
);
}
assert_eq!(nicedupstring("hé\nllo"), nicedup("hé\nllo", 1));
}
#[test]
fn zwcwidth_returns_1_when_multibyte_unset() {
let _g = crate::test_util::global_state_lock();
let saved = isset(MULTIBYTE);
dosetopt(MULTIBYTE, 0, 1);
assert_eq!(zwcwidth('a'), 1, "c:738 — ASCII width 1 under nomultibyte");
assert_eq!(
zwcwidth('字'),
1,
"c:738 — CJK collapses to 1 under nomultibyte (was 2 via Unicode-width)"
);
assert_eq!(
zwcwidth('\u{200B}'),
1,
"c:738 — zero-width space → 1 under nomultibyte (was 0 via Unicode-width)"
);
dosetopt(MULTIBYTE, 1, 1);
assert_eq!(zwcwidth('a'), 1, "ASCII width is 1");
assert_eq!(zwcwidth('字'), 2, "c:740 — CJK width 2 under multibyte");
dosetopt(MULTIBYTE, if saved { 1 } else { 0 }, 1);
}
#[test]
fn quotedzputs_uses_dollar_quotes_for_control_chars() {
let _g = crate::test_util::global_state_lock();
inittyptab();
assert_eq!(quotedzputs(""), "''", "c:6470-6475 — empty input → ''");
assert_eq!(
quotedzputs("hello"),
"hello",
"c:6511-6517 — no SPECCHARS member → return unchanged"
);
let r = quotedzputs("a\nb");
assert!(
r.starts_with("$'") && r.ends_with('\''),
"c:6488 — control char must use $'…' form (got {:?})",
r
);
let r = quotedzputs("\t");
assert!(
r.starts_with("$'"),
"c:6488 — TAB forces $'…' arm (got {:?})",
r
);
let r = quotedzputs("\u{1b}[31m");
assert!(
r.starts_with("$'"),
"c:6488 — ESC sequence forces $'…' arm (got {:?})",
r
);
let r = quotedzputs("a'b");
assert!(
r.contains("'\\''"),
"c:6573-6587 — embedded ' → '\\'' (got {:?})",
r
);
}
#[test]
#[cfg(unix)]
fn read_loop_round_trips_pipe_bytes() {
let _g = crate::test_util::global_state_lock();
let mut fds: [libc::c_int; 2] = [0; 2];
unsafe {
assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "pipe(2) ok");
}
let payload = b"hello-world-1234";
let written = unsafe {
libc::write(
fds[1],
payload.as_ptr() as *const libc::c_void,
payload.len(),
)
};
assert_eq!(written, payload.len() as isize, "write all 16 bytes");
unsafe {
libc::close(fds[1]);
}
let mut buf = [0u8; 16];
let got = read_loop(fds[0], &mut buf).expect("read_loop ok");
assert_eq!(got, payload.len(), "c:2929 — read_loop returns full length");
assert_eq!(
&buf[..],
&payload[..],
"c:2940-2941 — buffer copied verbatim"
);
unsafe {
libc::close(fds[0]);
}
}
#[test]
#[cfg(unix)]
fn write_loop_writes_all_bytes_to_pipe() {
let _g = crate::test_util::global_state_lock();
let mut fds: [libc::c_int; 2] = [0; 2];
unsafe {
assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "pipe(2) ok");
}
let payload = b"abcdef";
let got = write_loop(fds[1], payload).expect("write_loop ok");
assert_eq!(
got,
payload.len(),
"c:2955-2956 — write_loop returns full length on accept"
);
unsafe {
libc::close(fds[1]);
}
let mut buf = [0u8; 6];
let _ = unsafe { libc::read(fds[0], buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
assert_eq!(&buf[..], &payload[..]);
unsafe {
libc::close(fds[0]);
}
}
#[test]
#[cfg(unix)]
fn read_loop_returns_error_on_invalid_fd() {
let _g = crate::test_util::global_state_lock();
let mut buf = [0u8; 4];
let r = read_loop(9999, &mut buf);
assert!(
r.is_err(),
"c:2935 — invalid fd → io::Error (and zwarn to stderr)"
);
}
#[test]
#[cfg(unix)]
fn read1char_returns_minus_one_when_shtty_unset() {
let _g = crate::test_util::global_state_lock();
let saved = SHTTY.load(Ordering::Relaxed);
SHTTY.store(-1, Ordering::Relaxed);
let got = read1char(0); assert_eq!(got, -1, "c:2978 — SHTTY=-1 → read fails → return -1");
SHTTY.store(saved, Ordering::Relaxed);
}
#[test]
#[cfg(unix)]
fn movefd_marks_fdtable_internal() {
let _g = crate::test_util::global_state_lock();
let dev_null = CString::new("/dev/null").unwrap();
let fd = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
assert!(fd >= 0, "open /dev/null returned -1");
let new_fd = movefd(fd);
assert!(
new_fd >= 10,
"c:1992-1994 — movefd dups to fd >= 10 (got {})",
new_fd
);
let entry = fdtable_get(new_fd);
assert_eq!(
entry, FDT_INTERNAL,
"c:2009 — movefd must mark new_fd as FDT_INTERNAL (got {})",
entry
);
unsafe {
libc::close(new_fd);
}
}
#[test]
#[cfg(unix)]
fn movefd_minus_one_returns_minus_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
movefd(-1),
-1,
"c:1992 — movefd(-1) bypasses both gates and returns -1"
);
}
#[test]
#[cfg(unix)]
fn redup_copies_fdtable_ownership_to_target() {
let _g = crate::test_util::global_state_lock();
let dev_null = CString::new("/dev/null").unwrap();
let x = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
let y = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
assert!(x >= 0 && y >= 0, "open /dev/null returned -1");
assert_ne!(x, y);
check_fd_table(x);
check_fd_table(y);
fdtable_set(x, FDT_INTERNAL);
fdtable_set(y, FDT_UNUSED);
let ret = redup(x, y);
assert_eq!(ret, y, "c:2067 — successful redup returns y");
assert_eq!(
fdtable_get(y),
FDT_INTERNAL,
"c:2054 — fdtable[y] = fdtable[x] (FDT_INTERNAL)"
);
unsafe {
libc::close(y);
}
}
#[test]
#[cfg(unix)]
fn xsymlinks_follows_one_level_of_symlinks() {
let _g = crate::test_util::global_state_lock();
let tmp = std::env::temp_dir();
let target = tmp.join(format!("zshrs_xsymlinks_target_{}", std::process::id()));
let link = tmp.join(format!("zshrs_xsymlinks_link_{}", std::process::id()));
let _ = fs::create_dir(&target);
let _ = fs::remove_file(&link);
std::os::unix::fs::symlink(&target, &link).unwrap();
let got = xsymlinks(link.to_str().unwrap()).unwrap();
assert_eq!(
got,
target.to_string_lossy(),
"c:908 — xsymlinks must follow the symlink to its target"
);
let _ = fs::remove_file(&link);
let _ = fs::remove_dir(&target);
}
#[test]
#[cfg(unix)]
fn xsymlinks_normalises_dot_and_dotdot() {
let _g = crate::test_util::global_state_lock();
let tmp = fs::canonicalize(std::env::temp_dir()).unwrap();
let base_dir = tmp.join(format!("zshrs_xs_norm_{}", std::process::id()));
let _ = fs::create_dir(&base_dir);
let arg = format!("{}/./.", base_dir.display());
let got = xsymlinks(&arg).unwrap();
assert_eq!(
got,
base_dir.to_string_lossy(),
"c:881 — `.` segments collapse"
);
let arg = format!("{}/foo/..", base_dir.display());
let got = xsymlinks(&arg).unwrap();
assert_eq!(
got,
base_dir.to_string_lossy(),
"c:891-895 — `..` walks back one segment"
);
let _ = fs::remove_dir(&base_dir);
}
#[test]
#[cfg(unix)]
fn redup_promotes_flock_to_internal_on_target() {
let _g = crate::test_util::global_state_lock();
let dev_null = CString::new("/dev/null").unwrap();
let x = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
let y = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
assert!(x >= 0 && y >= 0);
assert_ne!(x, y);
check_fd_table(x);
check_fd_table(y);
fdtable_set(x, FDT_FLOCK);
FDTABLE_FLOCKS.store(2, Ordering::SeqCst);
let _ = redup(x, y);
assert_eq!(
fdtable_get(y),
FDT_INTERNAL,
"c:2055-2056 — FDT_FLOCK on x promotes to FDT_INTERNAL on y"
);
assert_eq!(
FDTABLE_FLOCKS.load(Ordering::SeqCst),
0,
"c:2062-2063 + c:2135 — flock count double-decremented (faithful to C)"
);
unsafe {
libc::close(y);
}
}
#[test]
#[cfg(unix)]
fn zreaddir_honors_ignoredots_flag() {
let _g = crate::test_util::global_state_lock();
let tmp = std::env::temp_dir();
let base = tmp.join(format!("zshrs_zreaddir_{}", std::process::id()));
let _ = fs::create_dir(&base);
fs::write(base.join("file"), "x").unwrap();
let mut dir = fs::read_dir(&base).unwrap();
let mut with_skip: Vec<String> = Vec::new();
while let Some(n) = zreaddir(&mut dir, 1) {
with_skip.push(n);
}
assert!(
with_skip.contains(&"file".to_string()),
"c:5232 — real entry survives ignoredots=1"
);
assert!(
!with_skip.contains(&".".to_string()),
"c:5232 — `.` filtered with ignoredots=1"
);
assert!(
!with_skip.contains(&"..".to_string()),
"c:5232 — `..` filtered with ignoredots=1"
);
let mut dir2 = fs::read_dir(&base).unwrap();
let mut without_skip: Vec<String> = Vec::new();
while let Some(n) = zreaddir(&mut dir2, 0) {
without_skip.push(n);
}
assert!(
without_skip.contains(&"file".to_string()),
"ignoredots=0 still yields real entries"
);
let _ = fs::remove_file(base.join("file"));
let _ = fs::remove_dir(&base);
}
#[test]
#[cfg(unix)]
fn ztrftime_zsh_extensions_no_leading_zero() {
let _g = crate::test_util::global_state_lock();
use std::time::Duration;
let t = UNIX_EPOCH + Duration::new(1704445662, 123_456_789);
let h_padded = ztrftime("%H", t);
let k_unpadded = ztrftime("%K", t);
if h_padded.starts_with('0') && h_padded.len() == 2 {
assert!(
!k_unpadded.starts_with('0'),
"c:3445 — %K must strip the leading 0 from %H={}, got %K={}",
h_padded,
k_unpadded
);
assert_eq!(
k_unpadded.len(),
1,
"c:3445 — %K should be 1 digit when hour < 10"
);
}
let d_padded = ztrftime("%d", t);
let f_unpadded = ztrftime("%f", t);
if d_padded.starts_with('0') && d_padded.len() == 2 {
assert!(!f_unpadded.starts_with('0'));
assert_eq!(
f_unpadded.len(),
1,
"c:3457 — %f should be 1 digit when day < 10"
);
}
let frac = ztrftime("%3.", t);
assert_eq!(
frac, "123",
"c:3409-3438 — %3. must emit first 3 digits of nsec"
);
let frac_default = ztrftime("%.", t);
assert_eq!(frac_default, "123", "c:3409 — %. defaults to 3 digits");
}
#[test]
#[cfg(unix)]
fn get_username_returns_metafied_non_empty_string() {
let _g = crate::test_util::global_state_lock();
let name = get_username();
if name.is_empty() {
return;
}
assert!(
!name.is_empty(),
"c:1086 — getpwuid result must yield a non-empty username"
);
if name.bytes().all(|b| b < 0x80) {
assert_eq!(
metafy(&name),
name,
"c:1086 — ASCII metafy is identity (so pin holds for ASCII users)"
);
}
}
#[test]
fn zstrtol_decimal_full_consumption() {
let (v, t) = zstrtol("42", 10);
assert_eq!(v, 42);
assert_eq!(t, "");
}
#[test]
fn zstrtol_decimal_stops_at_non_digit() {
let (v, t) = zstrtol("42abc", 10);
assert_eq!(v, 42);
assert_eq!(t, "abc");
}
#[test]
fn zstrtol_negative_decimal() {
let (v, t) = zstrtol("-7", 10);
assert_eq!(v, -7);
assert_eq!(t, "");
}
#[test]
fn zstrtol_explicit_plus_sign_consumed() {
let (v, t) = zstrtol("+12", 10);
assert_eq!(v, 12);
assert_eq!(t, "");
}
#[test]
fn zstrtol_hex_base_16() {
let (v, t) = zstrtol("ff", 16);
assert_eq!(v, 255);
assert_eq!(t, "");
}
#[test]
fn zstrtol_hex_uppercase() {
let (v, t) = zstrtol("FF", 16);
assert_eq!(v, 255);
}
#[test]
fn zstrtol_binary_base_2() {
let (v, t) = zstrtol("1010", 2);
assert_eq!(v, 10);
assert_eq!(t, "");
}
#[test]
fn zstrtol_octal_base_8() {
let (v, t) = zstrtol("17", 8);
assert_eq!(v, 15);
}
#[test]
fn zstrtol_base_zero_detects_hex_prefix() {
let (v, _) = zstrtol("0x1A", 0);
assert_eq!(v, 26, "0x1A with base=0 → hex 26");
}
#[test]
fn zstrtol_base_zero_detects_binary_prefix() {
let (v, _) = zstrtol("0b101", 0);
assert_eq!(v, 5, "0b101 with base=0 → binary 5");
}
#[test]
fn zstrtol_base_zero_leading_zero_means_octal() {
let (v, _) = zstrtol("017", 0);
assert_eq!(v, 15, "017 with base=0 → octal 15");
}
#[test]
fn zstrtol_leading_whitespace_skipped() {
let (v, _) = zstrtol(" 42", 10);
assert_eq!(v, 42);
}
#[test]
fn zstrtol_zero_input() {
let (v, t) = zstrtol("0", 10);
assert_eq!(v, 0);
assert_eq!(t, "");
}
#[test]
fn zstrtol_underscore_separator_in_decimal() {
let (v, _) = zstrtol_underscore("1_000_000", 10, true);
assert_eq!(v, 1_000_000);
}
#[test]
fn zstrtol_underscore_disabled_stops_at_underscore() {
let (v, t) = zstrtol_underscore("123_456", 10, false);
assert_eq!(v, 123);
assert_eq!(t, "_456");
}
#[test]
fn zstrtoul_underscore_basic_decimal() {
let v = zstrtoul_underscore("12345");
assert_eq!(v, Some(12345));
}
#[test]
fn zstrtoul_underscore_empty_returns_none() {
let v = zstrtoul_underscore("");
assert_eq!(v, None);
}
#[test]
fn getkeystring_decodes_common_escapes() {
assert_eq!(getkeystring("\\n").0, "\n");
assert_eq!(getkeystring("\\t").0, "\t");
assert_eq!(getkeystring("\\r").0, "\r");
}
#[test]
fn getkeystring_double_backslash_yields_single() {
assert_eq!(getkeystring("\\\\").0, "\\");
}
#[test]
fn getkeystring_hex_escape_lowercase_x() {
assert_eq!(getkeystring("\\x41").0, "A");
assert_eq!(getkeystring("\\x7E").0, "~");
}
#[test]
fn getkeystring_unicode_escape_u_four_digits() {
assert_eq!(getkeystring("\\u00E9").0, "é");
}
#[test]
fn getkeystring_plain_text_passes_through() {
assert_eq!(getkeystring("hello").0, "hello");
assert_eq!(getkeystring("").0, "");
}
#[test]
fn getkeystring_mixed_text_and_escapes() {
assert_eq!(getkeystring("a\\nb").0, "a\nb");
assert_eq!(
getkeystring("line1\\nline2\\tindented").0,
"line1\nline2\tindented"
);
}
#[test]
fn metafy_then_unmetafy_ascii_roundtrips_to_input() {
let s = "hello world";
let m = metafy(s);
assert_eq!(m, s);
let mut bytes = m.into_bytes();
let _len = unmetafy(&mut bytes);
assert_eq!(String::from_utf8(bytes).unwrap(), "hello world");
}
use crate::zsh_h::{
QT_BACKSLASH, QT_BACKSLASH_PATTERN, QT_BACKSLASH_SHOWNULL, QT_DOLLARS, QT_DOUBLE, QT_NONE,
QT_SINGLE, QT_SINGLE_OPTIONAL,
};
#[test]
fn quotestring_qt_none_passes_through_unchanged() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotestring("hello world", QT_NONE), "hello world");
assert_eq!(quotestring("", QT_NONE), "");
assert_eq!(quotestring("a*b?c", QT_NONE), "a*b?c");
}
#[test]
fn quotestring_qt_none_empty_is_empty() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotestring("", QT_NONE), "");
}
#[test]
fn quotestring_qt_backslash_empty_yields_empty_single_quotes() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotestring("", QT_BACKSLASH), "''");
}
#[test]
fn quotestring_qt_backslash_shownull_empty_yields_empty_single_quotes() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotestring("", QT_BACKSLASH_SHOWNULL), "''");
}
#[test]
fn quotestring_qt_single_empty_yields_empty_single_quotes() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotestring("", QT_SINGLE), "''");
}
#[test]
fn quotestring_qt_single_optional_empty_yields_empty_single_quotes() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotestring("", QT_SINGLE_OPTIONAL), "''");
}
#[test]
fn quotestring_qt_double_empty_yields_empty_double_quotes() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotestring("", QT_DOUBLE), "\"\"");
}
#[test]
fn quotestring_qt_dollars_empty_yields_dollar_quote_pair() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotestring("", QT_DOLLARS), "$''");
}
#[test]
fn quotestring_qt_backslash_pattern_escapes_glob_metas() {
let _g = crate::test_util::global_state_lock();
let r = quotestring("a*b?c[d]", QT_BACKSLASH_PATTERN);
assert!(
r.contains("\\*") && r.contains("\\?") && r.contains("\\[") && r.contains("\\]"),
"all globs must be escaped; got {r:?}"
);
}
#[test]
fn quotestring_qt_backslash_pattern_doesnt_escape_plain_chars() {
let _g = crate::test_util::global_state_lock();
let r = quotestring("plain", QT_BACKSLASH_PATTERN);
assert_eq!(r, "plain", "plain text passes through");
}
#[test]
fn quotestring_qt_backslash_pattern_escapes_all_meta_set() {
let _g = crate::test_util::global_state_lock();
for ch in ['<', '>', '(', ')', '|', '#', '^', '~'] {
let s = format!("x{ch}y");
let r = quotestring(&s, QT_BACKSLASH_PATTERN);
assert!(
r.contains(&format!("\\{ch}")),
"{ch:?} must be escaped, got {r:?}"
);
}
}
#[test]
fn quotestring_qt_single_wraps_simple_input() {
let _g = crate::test_util::global_state_lock();
assert_eq!(quotestring("simple", QT_SINGLE), "'simple'");
}
#[test]
fn metafy_ascii_is_identity_byte_for_byte() {
let s = "abc 123 XYZ!@#";
assert_eq!(metafy(s), s);
}
#[test]
fn metafy_empty_string_returns_empty() {
assert_eq!(metafy(""), "");
}
#[test]
fn metafy_unmetafy_roundtrip_with_punctuation() {
let s = "Hello, World! 123 +-=";
let m = metafy(s);
let mut bytes = m.into_bytes();
let _ = unmetafy(&mut bytes);
let result = String::from_utf8(bytes).expect("valid utf-8");
assert_eq!(result, s);
}
#[test]
fn metafy_unmetafy_roundtrip_with_utf8_multibyte_anchored() {
let s = "日本語";
let mut bytes: Vec<u8> = Vec::with_capacity(s.len());
for &b in s.as_bytes() {
if imeta_byte(b) {
bytes.push(Meta);
bytes.push(b ^ 32);
} else {
bytes.push(b);
}
}
let _ = unmetafy(&mut bytes);
let result = String::from_utf8(bytes).expect("valid utf-8");
assert_eq!(result, s, "UTF-8 round-trip must preserve content");
}
#[test]
fn quotestring_corpus_qt_none_is_identity() {
let s = "anything `goes` $here";
assert_eq!(quotestring(s, QT_NONE), s);
}
#[test]
fn quotestring_corpus_qt_single_wraps_plain_word() {
let out = quotestring("hello", QT_SINGLE);
assert!(
out.starts_with('\'') && out.ends_with('\''),
"single-quoted = wraps with ', got {out:?}"
);
assert!(out.contains("hello"), "content preserved");
}
#[test]
fn quotestring_corpus_qt_double_wraps_plain_word() {
let out = quotestring("hello", QT_DOUBLE);
assert!(
out.starts_with('"') && out.ends_with('"'),
"double-quoted = wraps with \", got {out:?}"
);
}
#[test]
fn quotestring_corpus_qt_single_escapes_apostrophe() {
let out = quotestring("it's", QT_SINGLE);
assert!(
out.contains("\\'") || out.contains("'\\''"),
"apostrophe gets escaped in single-quote form, got {out:?}",
);
}
#[test]
fn quotestring_corpus_qt_backslash_escapes_glob_chars() {
let out = quotestring("a*b?c", QT_BACKSLASH);
assert!(out.contains("\\*"), "* gets backslashed, got {out:?}");
assert!(out.contains("\\?"), "? gets backslashed, got {out:?}");
}
#[test]
fn quotestring_corpus_qt_double_empty_yields_double_quotes() {
let out = quotestring("", QT_DOUBLE);
assert_eq!(out, "\"\"", "empty double-quoted = \"\"");
}
#[test]
fn quotestring_corpus_qt_single_empty_yields_single_quotes() {
let out = quotestring("", QT_SINGLE);
assert_eq!(out, "''", "empty single-quoted = ''");
}
#[test]
fn quotestring_corpus_qt_backslash_plain_word_unchanged() {
let out = quotestring("abc123", QT_BACKSLASH);
assert_eq!(
out, "abc123",
"plain alphanumeric needs no escape, got {out:?}"
);
}
#[test]
fn ztrlen_corpus_plain_ascii_byte_count() {
assert_eq!(ztrlen("hello"), 5);
}
#[test]
fn ztrlen_corpus_empty_is_zero() {
assert_eq!(ztrlen(""), 0);
}
#[test]
fn itype_end_ident_walks_full_identifier() {
let _g = crate::test_util::global_state_lock();
inittyptab();
use crate::ported::ztype_h::IIDENT;
assert_eq!(itype_end("abc123", IIDENT as u32, false), 6);
}
#[test]
fn itype_end_ident_stops_at_dash() {
let _g = crate::test_util::global_state_lock();
inittyptab();
use crate::ported::ztype_h::IIDENT;
assert_eq!(itype_end("abc-def", IIDENT as u32, false), 3);
}
#[test]
fn itype_end_once_stops_after_first_match() {
let _g = crate::test_util::global_state_lock();
inittyptab();
use crate::ported::ztype_h::IIDENT;
assert_eq!(itype_end("abc", IIDENT as u32, true), 1);
}
#[test]
fn itype_end_empty_string_returns_zero() {
let _g = crate::test_util::global_state_lock();
inittyptab();
use crate::ported::ztype_h::IIDENT;
assert_eq!(itype_end("", IIDENT as u32, false), 0);
}
#[test]
fn itype_end_non_ident_first_char_returns_zero() {
let _g = crate::test_util::global_state_lock();
inittyptab();
use crate::ported::ztype_h::IIDENT;
assert_eq!(itype_end(":foo", IIDENT as u32, false), 0);
}
#[test]
fn itype_end_inamespc_walks_through_ksh93_dot() {
let _g = crate::test_util::global_state_lock();
inittyptab();
use crate::ported::ztype_h::INAMESPC;
assert_eq!(itype_end("ns.foo", INAMESPC, false), 6);
}
#[test]
fn itype_end_isep_walks_separator_run() {
let _g = crate::test_util::global_state_lock();
inittyptab();
use crate::ported::ztype_h::ISEP;
assert_eq!(itype_end(" \t a", ISEP as u32, false), 3);
}
#[test]
fn unmetafy_pure_ascii_unchanged() {
let _g = crate::test_util::global_state_lock();
let mut v = b"abc".to_vec();
let n = unmetafy(&mut v);
assert_eq!(n, 3);
assert_eq!(v, b"abc");
}
#[test]
fn unmetafy_meta_pair_collapses_to_xor_byte() {
let _g = crate::test_util::global_state_lock();
let mut v: Vec<u8> = vec![b'a', 0x83, b'a'];
let n = unmetafy(&mut v);
assert_eq!(n, 2);
assert_eq!(v, vec![b'a', b'A']);
}
#[test]
fn unmetafy_empty_returns_zero() {
let _g = crate::test_util::global_state_lock();
let mut v: Vec<u8> = Vec::new();
let n = unmetafy(&mut v);
assert_eq!(n, 0);
}
#[test]
fn unmetafy_trailing_lone_meta_preserved() {
let _g = crate::test_util::global_state_lock();
let mut v: Vec<u8> = vec![b'a', 0x83];
let n = unmetafy(&mut v);
assert_eq!(n, 2);
assert_eq!(v, vec![b'a', 0x83]);
}
#[test]
fn nicechar_printable_ascii_passes_through() {
let _g = crate::test_util::global_state_lock();
assert_eq!(nicechar('a'), "a");
}
#[test]
fn nicechar_newline_emits_escape() {
let _g = crate::test_util::global_state_lock();
let out = nicechar('\n');
assert!(
out == "\\n" || out == "\n",
"newline should be escaped or literal; got {out:?}"
);
}
#[test]
fn wcs_nicechar_printable_ascii_passes_through() {
let _g = crate::test_util::global_state_lock();
assert_eq!(wcs_nicechar('A', None, None), "A");
}
#[test]
fn nicechar_printable_ascii_passes_through_pin() {
let _g = crate::test_util::global_state_lock();
assert_eq!(nicechar('a'), "a");
assert_eq!(nicechar('Z'), "Z");
assert_eq!(nicechar('5'), "5");
assert_eq!(nicechar(' '), " ");
}
#[test]
fn nicechar_newline_returns_backslash_n() {
let _g = crate::test_util::global_state_lock();
assert_eq!(nicechar('\n'), "\\n");
}
#[test]
fn nicechar_tab_returns_backslash_t() {
let _g = crate::test_util::global_state_lock();
assert_eq!(nicechar('\t'), "\\t");
}
#[test]
fn nicechar_del_returns_caret_question() {
let _g = crate::test_util::global_state_lock();
assert_eq!(nicechar('\x7f'), "^?");
}
#[test]
fn nicechar_ctrl_a_returns_caret_A() {
let _g = crate::test_util::global_state_lock();
assert_eq!(nicechar('\x01'), "^A");
}
#[test]
fn nicechar_esc_returns_caret_bracket() {
let _g = crate::test_util::global_state_lock();
assert_eq!(nicechar('\x1b'), "^[");
}
#[test]
fn nicechar_sel_quotable_uses_backslash_C() {
let _g = crate::test_util::global_state_lock();
let r = nicechar_sel('\x01', true);
assert!(
r.contains("\\C-") || r.contains("^"),
"quotable should emit \\C- form, got {:?}",
r
);
}
#[test]
fn is_nicechar_printable_returns_false() {
let _g = crate::test_util::global_state_lock();
assert!(!is_nicechar('a'), "printable doesn't NEED nice repr");
assert!(!is_nicechar('Z'));
assert!(!is_nicechar('5'));
}
#[test]
fn is_nicechar_all_ascii_safe() {
let _g = crate::test_util::global_state_lock();
for c in 0u8..=127 {
let _ = is_nicechar(c as char); }
}
#[test]
fn nicechar_is_deterministic() {
let _g = crate::test_util::global_state_lock();
for c in ['a', '\n', '\t', '\x7f', '\x01', '\x1b'] {
let first = nicechar(c);
for _ in 0..5 {
assert_eq!(nicechar(c), first, "{:?} must be pure", c);
}
}
}
#[test]
fn pathprog_empty_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(pathprog("").is_none());
}
#[test]
fn pathprog_nonexistent_returns_none() {
let _g = crate::test_util::global_state_lock();
let r = pathprog("/__never_exists_zshrs_pathprog_xyz");
assert!(r.is_none());
}
#[test]
fn set_widearray_empty_returns_empty() {
let _g = crate::test_util::global_state_lock();
assert!(set_widearray("").is_empty());
}
#[test]
fn set_widearray_returns_vec_char_type() {
let _g = crate::test_util::global_state_lock();
let _: Vec<char> = set_widearray("a");
}
#[test]
fn nicechar_sel_ascii_letter_passthrough() {
assert_eq!(nicechar_sel('a', false), "a", "printable letter as-is");
}
#[test]
fn nicechar_sel_is_pure() {
for c in ['a', '\t', '\n', '\x1b', '\x7f', '日'] {
let first = nicechar_sel(c, false);
for _ in 0..3 {
assert_eq!(
nicechar_sel(c, false),
first,
"nicechar_sel({:?}, false) must be pure",
c
);
}
}
}
#[test]
fn zwcwidth_returns_usize_type() {
let _: usize = zwcwidth('a');
}
#[test]
fn zwcwidth_ascii_returns_one() {
assert_eq!(zwcwidth('a'), 1, "ASCII letter width = 1");
}
#[test]
fn findpwd_empty_returns_option_string_type() {
let _g = crate::test_util::global_state_lock();
let _: Option<String> = findpwd("");
}
#[test]
fn slashsplit_empty_returns_empty() {
assert!(slashsplit("").is_empty());
}
#[test]
fn slashsplit_simple_three_components() {
let r = slashsplit("a/b/c");
assert_eq!(r, vec!["a".to_string(), "b".to_string(), "c".to_string()]);
}
#[test]
fn dircmp_both_empty_returns_bool_type() {
let _g = crate::test_util::global_state_lock();
let _: bool = dircmp("", "");
}
#[test]
fn getnameddir_empty_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getnameddir("").is_none(), "empty → None");
}
#[test]
fn get_username_returns_string_type() {
let _g = crate::test_util::global_state_lock();
let _: String = get_username();
}
}