use std::io::{self, Write};
use std::path::{Path, PathBuf};
use crate::ported::zsh_h::{
isset, opt_name, AUTONAMEDIRS, BEEP, MULTIBYTE, POSIXIDENTIFIERS,
PRINTEIGHTBIT, XTRACE,
};
use std::sync::atomic::Ordering;
use crate::ported::zsh_h::{QT_NONE, QT_BACKSLASH, QT_SINGLE, QT_DOUBLE, QT_DOLLARS};
use crate::ported::zsh_h::{QT_BACKTICK, QT_SINGLE_OPTIONAL, QT_BACKSLASH_PATTERN, QT_BACKSLASH_SHOWNULL};
use std::os::unix::io::RawFd;
use std::time::UNIX_EPOCH;
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 std::io::Read;
use crate::ported::ztype_h::{TYPTAB, TYPTAB_FLAGS, ZTF_INIT, ICNTRL, IDIGIT, IALNUM, IWORD, IIDENT, IUSER, IALPHA, IBLANK, INBLANK, ITOK, IMETA, INULL};
use std::os::unix::fs::PermissionsExt;
use crate::ported::zsh_h::{hashnode, nameddir, ND_NOABBREV, ND_USERNAME};
use crate::ported::ztype_h::{ISPECIAL, ZTF_SP_COMMA};
use std::os::unix::fs::MetadataExt;
use crate::ported::ztype_h::ZTF_BANGCHAR;
use crate::ported::ztype_h::ISEP;
use std::ffi::CString;
use crate::ported::zsh_h::{EMULATE_KSH, EMULATE_SH, EMULATION};
use std::sync::Mutex;
use std::sync::atomic::AtomicI64;
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) {
let _ = unsafe { libc::isatty(2) };
let scriptname = scriptname_lock().lock().unwrap().clone();
let argzero = argzero_lock().lock().unwrap().clone();
let locallevel = crate::ported::params::locallevel
.load(std::sync::atomic::Ordering::Relaxed);
let shinstdin = crate::ported::zsh_h::isset(
crate::ported::zsh_h::SHINSTDIN
);
let prefix: String = scriptname
.or(argzero)
.unwrap_or_default();
let stderr_handle = std::io::stderr();
let mut stderr_lock = stderr_handle.lock();
if let Some(cmd) = cmd {
if !shinstdin || locallevel != 0 {
let _ = stderr_lock.write_all(nicezputs(&prefix).as_bytes());
let _ = stderr_lock.write_all(b":");
}
let _ = stderr_lock.write_all(nicezputs(cmd).as_bytes());
let _ = stderr_lock.write_all(b":");
} else {
let to_emit = if shinstdin && locallevel == 0 {
"zsh"
} else {
prefix.as_str()
};
let _ = stderr_lock.write_all(nicezputs(to_emit).as_bytes());
let _ = stderr_lock.write_all(b":");
}
let lineno = crate::ported::lex::lineno() as i32;
if (!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) {
#[cfg(debug_assertions)]
{
eprintln!("BUG: {}", msg);
}
#[cfg(not(debug_assertions))]
{
let _ = msg;
}
}
pub fn zz_plural_z_alpha() {}
pub fn is_nicechar(c: char) -> bool {
let cu = (c as u32) & 0xff;
if crate::ported::ztype_h::ZISPRINT(cu as u8) {
return false;
}
if (cu & 0x80) != 0 {
return !crate::ported::zsh_h::isset(crate::ported::zsh_h::PRINTEIGHTBIT);
}
cu == 0x7f || cu == b'\n' as u32 || cu == b'\t' as u32 || cu < 0x20
}
pub fn zerrmsg(msg: &str, errno: Option<i32>) { let lineno = crate::ported::lex::lineno() as i32;
let shinstdin = crate::ported::zsh_h::isset(
crate::ported::zsh_h::SHINSTDIN
);
let locallevel = crate::ported::params::locallevel
.load(std::sync::atomic::Ordering::Relaxed);
if (!shinstdin || locallevel != 0) && lineno != 0 {
eprint!("{}: ", lineno);
}
if let Some(e) = errno {
eprintln!("{}: {}", msg, std::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);
TERM_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); }
pub fn zdeleteterm() { static TERM_COUNT: std::sync::atomic::AtomicI32 =
std::sync::atomic::AtomicI32::new(0);
if TERM_COUNT.load(std::sync::atomic::Ordering::Relaxed) > 0 {
TERM_COUNT.fetch_sub(1, std::sync::atomic::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 !crate::ported::ztype_h::ZISPRINT(c as u8) { if c & 0x80 != 0 { if isset(PRINTEIGHTBIT) { } else {
out.push_str("\\M-"); c &= 0x7f; if crate::ported::ztype_h::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 mb_charinit() {
}
pub fn wcs_nicechar_sel(c: char, quotable: bool) -> String { let cv = c as u32;
let print_eightbit = isset(PRINTEIGHTBIT);
let is_printable = crate::ported::compat::u9_iswprint(c);
if !is_printable && (cv < 0x80 || !print_eightbit) {
if cv == 0x7f { return if quotable { "\\C-?".to_string() } else { "^?".to_string() };
} else if c == '\n' { return "\\n".to_string();
} else if c == '\t' { return "\\t".to_string();
} else if cv < 0x20 { let cc = (cv + 0x40) as u8 as char;
return if quotable {
format!("\\C-{}", cc)
} else {
format!("^{}", cc)
};
}
} else if cv < 0x80 {
return c.to_string();
}
if crate::ported::compat::u9_iswprint(c) { return c.to_string();
}
if cv >= 0x10000 { format!("\\U{:08x}", cv)
} else if cv >= 0x100 { format!("\\u{:04x}", cv)
} else {
nicechar_sel(c, quotable)
}
}
pub fn wcs_nicechar(c: char) -> String { wcs_nicechar_sel(c, 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(crate::ported::zsh_h::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 {
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) = crate::ported::params::getsparam("PATH") {
for dir in path_var.split(':') { let full_path = PathBuf::from(dir).join(prog);
let unmeta_path = crate::ported::utils::unmeta(
full_path.to_str().unwrap_or(""),
); if let Ok(meta) = std::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 = crate::ported::params::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 std::fs::metadata(pwd) {
Ok(m) => m,
Err(_) => return false,
};
let dot_meta = match std::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> { s.split('/')
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
}
pub fn xsymlinks(s: &str) -> std::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 mut result = Vec::new();
for component in path.split('/') {
match component {
"" | "." => continue,
".." => {
if !result.is_empty() && result.last() != Some(&"..") {
result.pop();
} else if result.is_empty() && !path.starts_with('/') {
result.push("..");
}
}
c => result.push(c),
}
}
if path.starts_with('/') {
Ok(format!("/{}", result.join("/")))
} else if result.is_empty() {
Ok(".".to_string())
} else {
Ok(result.join("/"))
}
}
pub fn xsymlink(path: &str) -> Option<String> { if !path.starts_with('/') {
return None;
}
match std::fs::canonicalize(path) {
Ok(p) => Some(p.to_string_lossy().into_owned()),
Err(_) => {
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 { let s_at_entry = s.to_string(); if let Ok(resolved) = std::fs::canonicalize(s) { let r = resolved.to_string_lossy().into_owned();
if r != s_at_entry { print!(" -> "); print!("{}", if r.is_empty() { "/" } else { &r }); }
}
}
let _ = std::io::stdout().flush();
}
pub fn fprintdir(s: &str) -> String { match finddir(s) { None => unmeta(s), Some(rendered) => rendered, }
}
pub fn substnamedir(s: &str) -> String { match finddir(s) { None => quotestring(s, crate::ported::zsh_h::QT_BACKSLASH), Some(rendered) => rendered, }
}
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 {
std::ffi::CStr::from_ptr((*pw).pw_name)
.to_string_lossy()
.into_owned()
}
};
*guard = Some((current_uid, name.clone()));
name
}
pub fn finddir_scan(path: &str) -> Option<(String, String)> { let table = crate::ported::hashnameddir::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 home = crate::ported::params::getsparam("HOME").unwrap_or_default(); 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 !crate::ported::zsh_h::interact() { return; } if let Ok(t) = crate::ported::hashnameddir::nameddirtab().lock() {
if (flags & ND_USERNAME) != 0 && t.contains_key(name) { return;
}
if !always
&& !isset(crate::ported::zsh_h::AUTONAMEDIRS)
&& !t.contains_key(name)
{ return;
}
}
if dir.is_empty()
|| !dir.starts_with('/')
|| dir.len() >= libc::PATH_MAX as usize {
let _ = crate::ported::hashnameddir::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) = crate::ported::hashnameddir::nameddirtab().lock() {
if let Some(nd) = t.get(name) { return Some(nd.dir.clone());
}
}
if let Some(s) = crate::ported::params::getsparam(name) {
if s.starts_with('/') {
adduserdir(name, &s, 0, true); return Some(s);
}
}
#[cfg(unix)]
{
let cn = std::ffi::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 crate::ported::zsh_h::isset(crate::ported::zsh_h::CHASELINKS) {
xsymlink(&raw_dir).unwrap_or(raw_dir)
} else {
raw_dir
};
adduserdir(name, &dir,
crate::ported::zsh_h::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, args: Option<&[String]>, arrayp: bool) -> i32 {
let mut stat: i32 = 1;
let shf_exists = crate::ported::hashtable::shfunctab_lock()
.read()
.map(|t| t.get(name).is_some())
.unwrap_or(false);
if shf_exists {
stat = 0;
}
if arrayp {
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 exists = crate::ported::hashtable::shfunctab_lock()
.read()
.map(|t| t.get(&fn_name).is_some())
.unwrap_or(false);
if exists {
stat = 0;
}
}
}
stat
}
pub fn preprompt() {
static LAST_PERIODIC: AtomicI64 = AtomicI64::new(0);
callhookfunc("precmd", None, true);
let period = crate::ported::params::getiparam("PERIOD");
if period > 0 {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
if now > LAST_PERIODIC.load(Ordering::Relaxed) + period
&& callhookfunc("periodic", None, true) == 0
{
LAST_PERIODIC.store(now, 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) = std::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 = crate::ported::params::getsparam("PS4")
.or_else(|| crate::ported::params::getsparam("PROMPT4"))
.unwrap_or_else(|| {
if posix {
"+ ".to_string() } else {
"+%N:%i> ".to_string() }
});
let saved = isset(XTRACE);
crate::ported::options::opt_state_set(
&opt_name(XTRACE), false,
);
let prefix = crate::prompt::expand_prompt(&prefix_template);
crate::ported::options::opt_state_set(
&opt_name(XTRACE), saved,
);
eprint!("{}", prefix);
}
pub fn freestr(_s: String) {}
pub fn gettempfile(prefix: Option<&str>) -> Option<(i32, String)> { #[cfg(unix)]
{
crate::ported::signals::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 std::ffi::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 = std::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); } crate::ported::signals::unqueue_signals(); result
}
#[cfg(not(unix))]
{
let _ = prefix;
None
}
}
#[cfg(unix)]
pub fn gettyinfo() -> Option<libc::termios> { fdgettyinfo(crate::ported::init::SHTTY.load(Ordering::Relaxed)).ok() }
#[cfg(unix)]
pub fn fdgettyinfo(fd: i32) -> std::io::Result<libc::termios> {
let mut tio: libc::termios = unsafe { std::mem::zeroed() };
if unsafe { libc::tcgetattr(fd, &mut tio) } == -1 {
Err(std::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(crate::ported::init::SHTTY.load(Ordering::Relaxed), ti).is_ok() }
#[cfg(unix)]
pub fn fdsettyinfo(SHTTY: i32, ti: &libc::termios) -> std::io::Result<()> {
loop {
if unsafe { libc::tcsetattr(SHTTY, libc::TCSADRAIN, ti) } != -1 {
return Ok(());
}
let err = std::io::Error::last_os_error();
if err.kind() != std::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;
}
}
}
crate::ported::params::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;
}
}
}
crate::ported::params::getsparam("COLUMNS")
.and_then(|s| s.parse().ok())
.unwrap_or(80)
}
pub fn adjustwinsize(from: i32) -> (usize, usize) { use std::sync::atomic::Ordering;
let getwinsz = ADJUSTWINSIZE_GETWINSZ.load(Ordering::SeqCst);
let mut ttyrows: i32 = 0;
let mut ttycols: i32 = 0;
if getwinsz != 0 || from == 1 { let shtty = crate::ported::init::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() {
crate::ported::params::setiparam("LINES", lines as i64); }
let cols = adjustcolumns() as i32;
if std::env::var_os("COLUMNS").is_some() {
crate::ported::params::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 {
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; }
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; }
zclose(x); }
}
#[cfg(not(unix))]
{
let _ = (x, y);
}
ret }
pub fn addmodulefd(fd: i32, fdt: i32) { if fd >= 0 {
fdtable_set(fd, fdt);
}
}
pub fn addlockfd(fd: i32, cloexec: bool) { use crate::ported::zsh_h::{FDT_FLOCK, FDT_FLOCK_EXEC};
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(std::sync::atomic::Ordering::Relaxed); if fd <= max_fd { if fdtable_get(fd) == crate::ported::zsh_h::FDT_FLOCK { FDTABLE_FLOCKS.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); }
fdtable_set(fd, crate::ported::zsh_h::FDT_UNUSED); let mut m = MAX_ZSH_FD.load(std::sync::atomic::Ordering::Relaxed);
while m > 0 && fdtable_get(m) == crate::ported::zsh_h::FDT_UNUSED {
m -= 1;
}
MAX_ZSH_FD.store(m, std::sync::atomic::Ordering::Relaxed);
if fd == crate::ported::modules::clone::coprocin.load(std::sync::atomic::Ordering::Relaxed) {
crate::ported::modules::clone::coprocin.store(-1, std::sync::atomic::Ordering::Relaxed);
}
if fd == crate::ported::modules::clone::coprocout.load(std::sync::atomic::Ordering::Relaxed) {
crate::ported::modules::clone::coprocout.store(-1, std::sync::atomic::Ordering::Relaxed);
}
}
#[cfg(unix)]
unsafe { return libc::close(fd); } #[cfg(not(unix))]
return 0;
}
-1 }
pub fn zcloselockfd(fd: i32) -> i32 { use crate::ported::zsh_h::{FDT_FLOCK, FDT_FLOCK_EXEC};
let max_fd = MAX_ZSH_FD.load(std::sync::atomic::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" }; crate::ported::signals::queue_signals(); let prefix_owned: String = match prefix { Some(p) => p.to_string(),
None => crate::ported::params::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(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let unique = format!("{:x}{:x}", pid, nanos & 0xffffff);
let name = template.replace("XXXXXX", &unique);
crate::ported::signals::unqueue_signals(); Some(name)
}
pub fn has_token(s: &str) -> bool { s.bytes().any(crate::ported::ztype_h::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: &str, n: usize) -> String {
s.chars().take(n).collect()
}
pub fn strucpy(dest: &mut String, t: &str) { dest.push_str(t); }
pub fn struncpy(dest: &mut String, t: &str, n: usize) { let take = n.min(t.len());
dest.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(s: &str, open: char, close: char) -> usize { let mut depth = 0;
for (i, c) in s.char_indices() {
if c == open {
depth += 1;
} else if c == close {
depth -= 1;
if depth == 0 {
return i + c.len_utf8();
}
}
}
s.len()
}
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
&& crate::ported::zsh_h::isset(crate::ported::zsh_h::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() -> i64 { #[cfg(unix)]
{
let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 };
unsafe {
libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts); }
ts.tv_sec as i64 }
#[cfg(not(unix))]
{
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 = std::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(); 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 {
if let Some(c) = getquery(
&format!("zsh: sure you want to delete all of {}? [yn] ", s),
"yn",
) {
c == 'y' || c == 'Y'
} else {
false
}
}
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;
}
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;
}
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() -> Option<char> {
#[cfg(unix)]
{
let mut buf = [0u8; 1];
if std::io::stdin().read_exact(&mut buf).is_ok() {
return Some(buf[0] as char);
}
}
None
}
pub fn noquery(purge: bool) -> i32 { let mut val: libc::c_int = 0; #[cfg(unix)]
{
let shtty = crate::ported::init::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(prompt: &str, valid_chars: &str) -> Option<char> {
eprint!("{}", prompt);
let _ = io::stderr().flush();
let mut buf = [0u8; 1];
#[cfg(unix)]
{
if std::io::stdin().read_exact(&mut buf).is_ok() {
let c = buf[0] as char;
if valid_chars.is_empty() || valid_chars.contains(c) {
return Some(c);
}
}
}
None
}
pub fn spscan(name: &str, candidates: &[String], threshold: usize) -> Option<String> {
let mut best = None;
let mut best_dist = threshold + 1;
for candidate in candidates {
let dist = spdist(name, candidate, threshold);
if dist < best_dist {
best_dist = dist;
best = Some(candidate.clone());
}
}
best
}
pub fn spckword(word: &str, candidates: &[&str], threshold: usize) -> Option<String> { let mut best = None;
let mut best_dist = threshold + 1;
for &candidate in candidates {
let dist = spdist(word, candidate, threshold);
if dist < best_dist {
best_dist = dist;
best = Some(candidate.to_string());
}
}
best
}
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;
#[cfg(unix)]
unsafe {
let tm = libc::localtime(&secs);
if tm.is_null() {
return String::new();
}
let mut buf = vec![0u8; 256];
let c_fmt = std::ffi::CString::new(fmt).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);
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_byte(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] as char) {
s_pos += 1;
}
}
let has_word_now = s_pos < bytes.len()
&& !iwsep(bytes[s_pos] as char);
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] as char) {
s_pos += 1;
}
if s_pos > word_start && mul <= 0 {
while s_pos < bytes.len() && iwsep(bytes[s_pos] as char) {
s_pos += 1;
}
}
if s_pos < bytes.len() && iwsep(bytes[s_pos] as char) {
s_pos += 1;
}
let t_after = s_pos;
if mul <= 0 {
while s_pos < bytes.len() && iwsep(bytes[s_pos] as char) {
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 sep = sep.unwrap_or(" ");
arr.join(sep)
}
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<String> {
let tab = crate::ported::hashtable::shfunctab_lock()
.read()
.expect("shfunctab poisoned");
tab.get(nam).and_then(|f| f.body.clone())
}
pub fn subst_string_by_func(func_name: &str, arg1: Option<&str>, orig: &str)
-> Option<Vec<String>> {
let osc = crate::ported::builtin::SFCONTEXT.load(Ordering::Relaxed); let osm = crate::ported::builtin::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()); crate::ported::builtin::SFCONTEXT .store(crate::ported::zsh_h::SFC_SUBST, Ordering::Relaxed);
INCOMPFUNC.store(0, Ordering::Relaxed);
let rc = callhookfunc(func_name, Some(&args), false); let ret: Option<Vec<String>> = if rc != 0 {
None } else {
crate::ported::params::getaparam("reply") };
crate::ported::builtin::SFCONTEXT.store(osc, Ordering::Relaxed); crate::ported::builtin::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) = crate::ported::params::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() { crate::ported::signals::queue_signals(); if let Ok(zbeep) = std::env::var("ZBEEP") { let (decoded, _) = getkeystring(&zbeep); #[cfg(unix)]
{
let shtty = crate::ported::init::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(crate::ported::zsh_h::BEEP) { #[cfg(unix)]
{
let shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
if shtty != -1 {
let _ = write_loop(shtty, b"\x07"); } else {
eprint!("\x07");
}
}
#[cfg(not(unix))]
eprint!("\x07");
}
crate::ported::signals::unqueue_signals(); }
pub fn freearray(s: Vec<String>) {
}
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; {
use crate::ported::zsh_h::{Marker, META};
t[META as usize] |= IMETA as u32;
t[Marker as usize] |= IMETA as u32;
}
{
use crate::ported::zsh_h::{LAST_NORMAL_TOK, Pound};
let lo = Pound as usize;
let hi = LAST_NORMAL_TOK as usize;
for t0 in lo..=hi {
t[t0] |= (ITOK | IMETA) as u32;
}
}
{
use crate::ported::zsh_h::{Nularg, Snull};
let lo = Snull as usize;
let hi = Nularg as usize;
for t0 in lo..=hi {
t[t0] |= (ITOK | IMETA | INULL) as u32;
}
}
{
use crate::ported::ztype_h::{ISEP as ZT_ISEP, IWSEP as ZT_IWSEP};
use crate::ported::zsh_h::{DEFAULT_IFS, META};
let ifs = crate::ported::params::ifsgetfn();
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 as u8 && 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] |= ZT_IWSEP as u32; }
}
t[cu] |= ZT_ISEP as u32;
i += 1;
}
}
{
use crate::ported::ztype_h::IWORD as ZT_IWORD;
use crate::ported::zsh_system_h::DEFAULT_WORDCHARS;
use crate::ported::zsh_h::META;
let wc = crate::ported::params::wordcharsgetfn();
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 as u8 && i + 1 < bytes.len() {
i += 1;
bytes[i] ^ 32
} else {
bytes[i]
};
if c < 0x80 {
t[c as usize] |= ZT_IWORD as u32; }
i += 1;
}
}
{
use crate::ported::ztype_h::ISPECIAL as ZT_ISPECIAL;
use crate::ported::zsh_h::SPECCHARS;
for &b in SPECCHARS.as_bytes() {
t[b as usize] |= ZT_ISPECIAL as u32; }
}
{
use crate::ported::ztype_h::{ISPECIAL as ZT_ISPECIAL, ZTF_SP_COMMA};
let flags = *TYPTAB_FLAGS.lock().unwrap();
if (flags & ZTF_SP_COMMA) != 0 { t[b',' as usize] |= ZT_ISPECIAL as u32; }
}
{
use crate::ported::ztype_h::{ISPECIAL as ZT_ISPECIAL, ZTF_BANGCHAR, ZTF_INTERACT};
let bangchar = crate::ported::hist::bangchar.load(Ordering::SeqCst) as usize;
let flags = *TYPTAB_FLAGS.lock().unwrap();
let interact_flag = (flags & ZTF_INTERACT) != 0;
let banghist = crate::ported::zsh_h::isset(crate::ported::zsh_h::BANGHIST);
if banghist && bangchar != 0 && bangchar < 256 && interact_flag { *TYPTAB_FLAGS.lock().unwrap() |= ZTF_BANGCHAR; t[bangchar] |= ZT_ISPECIAL as u32; } else {
*TYPTAB_FLAGS.lock().unwrap() &= !ZTF_BANGCHAR; }
}
{
use crate::ported::ztype_h::IPATTERN as ZT_IPATTERN;
use crate::ported::zsh_h::PATCHARS;
for &b in PATCHARS.as_bytes() {
t[b as usize] |= ZT_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 = crate::ported::hist::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(crate::ported::zsh_h::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(crate::ported::zsh_h::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::wordcharsgetfn();
return w.chars().any(|x| x == c);
}
if cls == ISEP { let ifs = crate::ported::params::ifsgetfn();
return ifs.chars().any(|x| x == c);
}
let _ = IALNUM;
c.is_alphanumeric() }
pub fn itype_end(s: &str, allow_digits_start: bool) -> usize {
let mut chars = s.chars().peekable();
let mut pos = 0;
if let Some(&first) = chars.peek() {
if !allow_digits_start && first.is_ascii_digit() {
return 0;
}
if !first.is_alphanumeric() && first != '_' && first != '.' {
return 0;
}
}
for c in s.chars() {
if c.is_alphanumeric() || c == '_' || c == '.' {
pos += c.len_utf8();
} else {
break;
}
}
pos
}
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 std::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 std::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, max_dist: usize) -> usize { let s_chars: Vec<char> = s.chars().collect();
let t_chars: Vec<char> = t.chars().collect();
let m = s_chars.len();
let n = t_chars.len();
if m.abs_diff(n) > max_dist {
return max_dist + 1;
}
let mut prev: Vec<usize> = (0..=n).collect();
let mut curr = vec![0; n + 1];
for i in 1..=m {
curr[0] = i;
for j in 1..=n {
let cost = if s_chars[i - 1] == t_chars[j - 1] {
0
} else {
1
};
curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[n]
}
#[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 !(crate::ported::zsh_h::jobbing() && crate::ported::zsh_h::interact()) {
return; }
let shtty = crate::ported::init::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(|| std::sync::Mutex::new(0))
.lock().unwrap();
if pgrp != mypgrp_val
&& unsafe { libc::kill(-pgrp, 0) } == -1
{
attachtty(mypgrp_val); } else {
let errno_val = std::io::Error::last_os_error().raw_os_error()
.unwrap_or(0);
if errno_val != libc::ENOTTY { zwarn(&format!("can't set tty pgrp: {}", std::io::Error::from_raw_os_error(errno_val)));
let _ = std::io::stderr().flush(); }
crate::ported::options::opt_state_set("monitor", false); ATTACHTTY_EP.store(1, Ordering::Relaxed); }
} else if rc != -1 { *crate::ported::jobs::LAST_ATTACHED_PGRP .get_or_init(|| std::sync::Mutex::new(0))
.lock().unwrap() = pgrp;
}
}
#[cfg(unix)]
pub fn gettygrp() -> i32 { let shtty = crate::ported::init::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 as u8 { 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 as u8 && 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 as u8 { i += 2; l -= 1; } else {
i += 1;
}
}
l.max(0) as usize
}
pub fn zreaddir(path: &str) -> Vec<String> {
match std::fs::read_dir(path) {
Ok(entries) => entries
.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().into_string().ok())
.filter(|s| s != "." && s != "..")
.collect(),
Err(_) => Vec::new(),
}
}
pub fn zputs(s: &str) -> std::io::Result<()> { let mut out = String::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() { let c = bytes[i];
if c == Meta as u8 { if i + 1 < bytes.len() {
let decoded = bytes[i + 1] ^ 32;
out.push(decoded as char);
i += 2; } else {
i += 1;
}
} else if crate::ported::ztype_h::itok(c) { i += 1;
continue;
} else {
out.push(c as char);
i += 1;
}
}
std::io::stdout().lock().write_all(out.as_bytes()) }
pub fn nicedup(s: &str) -> String {
sb_niceformat(s)
}
pub fn nicedupstring(s: &str) -> String {
sb_niceformat(s)
}
pub fn nicezputs(s: &str) -> String { s.chars().map(nicechar).collect()
}
pub fn niceztrlen(s: &str) -> usize {
sb_niceformat(s).len()
}
pub fn mb_niceformat(s: &str) -> String {
let unmeta = self::unmeta(s);
let mut out = String::with_capacity(unmeta.len());
for c in unmeta.chars() {
out.push_str(&wcs_nicechar(c));
}
out
}
pub fn is_mb_niceformat(s: &str) -> bool {
let mut bytes = s.as_bytes().to_vec();
let umlen = unmetafy(&mut bytes);
bytes.truncate(umlen);
let mut i = 0;
while i < bytes.len() {
let remaining = &bytes[i..];
match std::str::from_utf8(remaining) {
Ok(s) => {
for c in s.chars() {
if is_wcs_nicechar(c) { return true;
}
}
return false;
}
Err(e) => {
let valid_up_to = e.valid_up_to();
if valid_up_to > 0 {
let valid = std::str::from_utf8(&remaining[..valid_up_to])
.expect("valid_up_to slice");
for c in valid.chars() {
if is_wcs_nicechar(c) { return true;
}
}
i += valid_up_to;
continue;
}
let b = remaining[0];
if is_nicechar(b as char) { return true;
}
i += 1;
}
}
}
false
}
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 as u8 && 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) -> String { let mut bytes = s.as_bytes().to_vec();
let umlen = unmetafy(&mut bytes);
bytes.truncate(umlen);
let mut result = String::new();
for &b in &bytes {
result.push_str(&nicechar_sel(b as char, false));
}
result
}
pub fn is_sb_niceformat(s: &str) -> bool { let mut bytes = s.as_bytes().to_vec();
let umlen = unmetafy(&mut bytes);
bytes.truncate(umlen);
bytes.iter().any(|&b| is_nicechar(b as char))
}
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 as u8 && 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);
for c in s.chars() {
if ispecial(c) {
result.push('\\');
}
result.push(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 if c == '\n' {
result.push_str("'$'\\n''");
} 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 = String::with_capacity(s.len() + 4);
let mut in_quotes = false;
for c in s.chars() {
if c == '\'' {
if in_quotes { result.push('\''); in_quotes = false; }
result.push_str("\\'");
} else if ispecial(c) {
if !in_quotes { result.push('\''); in_quotes = true; }
result.push(c);
} else {
if in_quotes { result.push('\''); in_quotes = false; }
result.push(c);
}
}
if in_quotes { result.push('\''); }
result
} 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()
}
}
#[allow(non_snake_case)]
pub(crate) fn quotedzputs(s: &str) -> String { if s.is_empty() {
return "''".to_string();
}
if !hasspecial(s) {
return s.to_string();
}
let inner = s.replace('\'', "'\\''");
format!("'{}'", inner)
}
pub fn dquotedztrdup(s: &str) -> String { let mut out = String::with_capacity(s.len() * 4 + 2);
let bytes = s.as_bytes();
if isset(crate::ported::zsh_h::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(codepoint: u32) -> Option<String> {
char::from_u32(codepoint).map(|c| c.to_string())
}
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 = [0i8; 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(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() -> crate::ported::zsh_h::dirsav { crate::ported::zsh_h::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 crate::ported::zsh_h::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) = std::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(unix)]
#[cfg(not(unix))]
{
let _ = mode;
return 0;
}
#[cfg(unix)]
{
let m = mode as u32;
let mut o: i32 = 0;
if m & S_ISUID as u32 != 0 { o |= 0o4000; }
if m & S_ISGID as u32 != 0 { o |= 0o2000; }
if m & S_ISVTX as u32 != 0 { o |= 0o1000; }
if m & S_IRUSR as u32 != 0 { o |= 0o0400; }
if m & S_IWUSR as u32 != 0 { o |= 0o0200; }
if m & S_IXUSR as u32 != 0 { o |= 0o0100; }
if m & S_IRGRP as u32 != 0 { o |= 0o0040; }
if m & S_IWGRP as u32 != 0 { o |= 0o0020; }
if m & S_IXGRP as u32 != 0 { o |= 0o0010; }
if m & S_IROTH as u32 != 0 { o |= 0o0004; }
if m & S_IWOTH as u32 != 0 { o |= 0o0002; }
if m & S_IXOTH as u32 != 0 { o |= 0o0001; }
o
}
}
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 std::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<std::sync::Mutex<Option<String>>> =
std::sync::OnceLock::new();
static ARGZERO: std::sync::OnceLock<std::sync::Mutex<Option<String>>> =
std::sync::OnceLock::new();
static POSIXZERO: std::sync::OnceLock<std::sync::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<std::sync::Mutex<i32>> = std::sync::OnceLock::new();
static LINENO: std::sync::OnceLock<std::sync::Mutex<i32>> = std::sync::OnceLock::new();
static SHINSTDIN_OPT: std::sync::OnceLock<std::sync::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_BACKSLASH_C: u32 = 1 << 3;
pub const GETKEYS_PRINT: u32 = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_BACKSLASH_C;
static ATTACHTTY_EP: std::sync::atomic::AtomicI32 =
std::sync::atomic::AtomicI32::new(0);
static FDTABLE: std::sync::OnceLock<std::sync::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);
#[allow(non_upper_case_globals)]
pub const Meta: u8 = 0x83;
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 locallevel() -> i32 {
crate::ported::params::locallevel.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn inc_locallevel() {
crate::ported::params::locallevel.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
pub fn dec_locallevel() {
crate::ported::params::locallevel.fetch_sub(1, std::sync::atomic::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) {
crate::ported::params::locallevel.store(v, std::sync::atomic::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 isident(s: &str) -> bool { let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
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 { crate::ported::params::convbase(val, base)
}
pub fn checkglobqual(s: &str) -> bool { if !s.ends_with(')') {
return false;
}
let mut depth = 0;
let mut in_bracket = false;
for c in s.chars() {
match c {
'[' if !in_bracket => in_bracket = true,
']' if in_bracket => in_bracket = false,
'(' if !in_bracket => depth += 1,
')' if !in_bracket => {
if depth > 0 {
depth -= 1;
} else {
return false;
}
}
_ => {}
}
}
depth == 0
}
pub fn dupstrpfx(s: &str, len: usize) -> String { let bytes = s.as_bytes();
let take = len.min(bytes.len());
String::from_utf8_lossy(&bytes[..take]).into_owned()
}
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 ztrdup(s: &str) -> String { s.to_string()
}
pub fn dyncat(s1: &str, s2: &str) -> String { format!("{}{}", s1, s2)
}
pub fn tricat(s1: &str, s2: &str, s3: &str) -> String { format!("{}{}{}", s1, s2, s3)
}
pub fn bicat(s1: &str, s2: &str) -> String { format!("{}{}", s1, s2)
}
pub fn iwsep(c: char) -> bool {
c == ' ' || c == '\t' || c == '\n'
}
#[inline]
pub fn iwsep_byte(b: u8) -> bool {
b == b' ' || b == b'\t' || b == b'\n'
}
pub fn imeta(c: char) -> bool {
(c as u32) >= Meta as u32
}
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();
}
}
}
crate::ported::params::getsparam("HOST")
.or_else(|| crate::ported::params::getsparam("HOSTNAME"))
.unwrap_or_else(|| "localhost".to_string())
}
pub fn zgetcwd() -> Option<String> { crate::ported::compat::zgetcwd()
}
pub fn realpath(path: &str) -> Option<String> {
std::fs::canonicalize(path)
.ok()
.map(|p| p.to_string_lossy().to_string())
}
pub fn mkdir(path: &str) -> bool {
std::fs::create_dir(path).is_ok()
}
pub fn symlink(src: &str, dst: &str) -> bool {
#[cfg(unix)]
{
std::os::unix::fs::symlink(src, dst).is_ok()
}
#[cfg(not(unix))]
{
let _ = (src, dst);
false
}
}
pub fn readlink(path: &str) -> Option<String> {
std::fs::read_link(path)
.ok()
.map(|p| p.to_string_lossy().to_string())
}
pub fn getenv(name: &str) -> Option<String> {
std::env::var(name).ok()
}
static PREPROMPT_FNS: std::sync::Mutex<Vec<fn()>> = std::sync::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);
}
static TIMED_FNS: std::sync::Mutex<Vec<(i64, fn())>> = std::sync::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('\'') => { 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(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);
}
}
Some(c) => {
consumed += 1;
if (how & GETKEY_EMACS) == 0 {
result.push('\\');
}
result.push(c);
}
None => { result.push('\\'); }
}
}
(result, consumed)
}
pub fn fdtable_get(fd: i32) -> i32 { if fd < 0 { return crate::ported::zsh_h::FDT_UNUSED; }
let g = fdtable_lock().lock().unwrap();
g.get(fd as usize).copied().unwrap_or(crate::ported::zsh_h::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, crate::ported::zsh_h::FDT_UNUSED);
}
g[fd as usize] = kind;
let cur = MAX_ZSH_FD.load(std::sync::atomic::Ordering::Relaxed);
if fd > cur {
MAX_ZSH_FD.store(fd, std::sync::atomic::Ordering::Relaxed);
}
}
#[inline]
pub fn imeta_byte(b: u8) -> bool {
b == 0 || (0x83..=0xa2).contains(&b)
}
pub fn convfloat(dval: f64, digits: i32, flags: u32) -> String {
crate::params::convfloat(dval, digits, flags)
}
pub fn convfloat_underscore(dval: f64, underscore: i32) -> String {
crate::params::convfloat_underscore(dval, underscore)
}
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 std::sync::Mutex<Option<String>> {
SCRIPTNAME.get_or_init(|| std::sync::Mutex::new(None))
}
fn argzero_lock() -> &'static std::sync::Mutex<Option<String>> {
ARGZERO.get_or_init(|| std::sync::Mutex::new(None))
}
fn posixzero_lock() -> &'static std::sync::Mutex<Option<String>> {
POSIXZERO.get_or_init(|| std::sync::Mutex::new(None))
}
fn noerrs_lock() -> &'static std::sync::Mutex<i32> {
NOERRS.get_or_init(|| std::sync::Mutex::new(0))
}
fn lineno_lock() -> &'static std::sync::Mutex<i32> {
LINENO.get_or_init(|| std::sync::Mutex::new(0))
}
fn shinstdin_lock() -> &'static std::sync::Mutex<bool> {
SHINSTDIN_OPT.get_or_init(|| std::sync::Mutex::new(false))
}
fn fdtable_lock() -> &'static std::sync::Mutex<Vec<i32>> {
FDTABLE.get_or_init(|| std::sync::Mutex::new(Vec::new()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn getaparam_reads_reply_from_paramtab_not_env() {
let saved = crate::ported::params::getaparam("reply");
let payload = vec![
"abbreviated".to_string(),
"11".to_string(),
];
let _ = crate::ported::params::setaparam("reply", payload.clone());
assert_eq!(
crate::ported::params::getaparam("reply"),
Some(payload),
"getaparam(\"reply\") must return paramtab array");
let _ = crate::ported::params::setaparam(
"reply", saved.unwrap_or_default());
}
#[test]
fn finddir_uses_paramtab_home_not_env() {
let saved = crate::ported::params::homegetfn();
let sentinel = "/tmp/zshrs-finddir-pin".to_string();
crate::ported::params::homesetfn(sentinel.clone());
let abbrev = super::finddir(&format!("{}/x", sentinel));
assert_eq!(abbrev.as_deref(), Some("~/x"),
"finddir must consult canonical HOME (got {:?})",
abbrev);
crate::ported::params::homesetfn(saved);
}
#[test]
fn test_sepsplit() {
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 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 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 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 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() {
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() {
assert_eq!(Meta, 0x83);
}
#[test]
#[cfg(unix)]
fn test_mode_to_octal_canonical_bits() {
use libc::{S_IRUSR, S_IWUSR, S_IXUSR};
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 = (libc::S_IRUSR
| libc::S_IWUSR
| libc::S_IXUSR
| libc::S_IRGRP
| libc::S_IWGRP
| libc::S_IXGRP
| libc::S_IROTH
| libc::S_IWOTH
| libc::S_IXOTH) as u32;
assert_eq!(mode_to_octal(m), 0o777);
let _ = all;
}
#[test]
#[cfg(unix)]
fn test_mode_to_octal_setuid_setgid_sticky() {
use libc::{S_ISGID, S_ISUID, S_ISVTX};
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() {
use std::os::unix::fs::MetadataExt;
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, std::fs::metadata("/etc/hosts").unwrap().nlink());
}
}
#[test]
fn test_mailstat_nonexistent_returns_neg1() {
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 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() {
assert_eq!(dupstrpfx("hello", 3), "hel");
assert_eq!(dupstrpfx("hi", 10), "hi");
assert_eq!(dupstrpfx("anything", 0), "");
}
#[test]
fn test_metafy_passes_through_ascii() {
assert_eq!(metafy("hello"), "hello");
assert_eq!(metafy(""), "");
}
#[test]
fn test_metafy_imeta_predicate_matches_c_macro() {
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 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() {
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 (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() {
assert!(!imeta(0x82 as char));
assert!(imeta(Meta as char));
assert!(imeta(0xff as char));
assert!(!imeta(' '));
assert!(!imeta('A'));
}
#[test]
fn test_unmeta_routes_through_unmetafy() {
assert_eq!(unmeta("plain"), "plain");
}
#[test]
fn test_iwsep_includes_newline() {
assert!(iwsep('\n'));
assert!(iwsep('\t'));
assert!(iwsep(' '));
assert!(!iwsep('a'));
}
#[test]
fn test_mailstat_aggregates_maildir() {
use std::fs;
use std::io::Write;
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() {
assert_eq!(spacesplit("a b c", false), vec!["a", "b", "c"]);
assert_eq!(spacesplit("a b", false), vec!["a", "b"]);
}
#[test]
fn test_sepjoin() {
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() {
assert!(isident("foo"));
assert!(isident("_bar"));
assert!(isident("baz123"));
assert!(!isident("123abc"));
assert!(!isident("foo-bar"));
}
#[test]
fn test_nicechar() {
assert_eq!(nicechar('\n'), "\\n");
assert_eq!(nicechar('\t'), "\\t");
assert_eq!(nicechar('a'), "a");
}
#[test]
fn test_quotedzputs_single_quote_wrap() {
assert_eq!(quotedzputs("simple"), "simple");
assert_eq!(quotedzputs("has space"), "'has space'");
assert_eq!(quotedzputs("it's"), "'it'\\''s'");
}
#[test]
fn test_quotestring_backslash() {
assert_eq!(quotestring("hello", crate::ported::zsh_h::QT_BACKSLASH), "hello");
assert_eq!(
quotestring("has space", crate::ported::zsh_h::QT_BACKSLASH),
"has\\ space"
);
assert_eq!(quotestring("$var", crate::ported::zsh_h::QT_BACKSLASH), "\\$var");
}
#[test]
fn quotestring_backslash_only_specchars_no_bang_in_default() {
use crate::ported::zsh_h::QT_BACKSLASH;
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() {
assert_eq!(quotestring("hello", crate::ported::zsh_h::QT_SINGLE), "'hello'");
assert_eq!(quotestring("it's", crate::ported::zsh_h::QT_SINGLE), "'it'\\''s'");
}
#[test]
fn test_quotestring_double() {
assert_eq!(quotestring("hello", crate::ported::zsh_h::QT_DOUBLE), "\"hello\"");
assert_eq!(
quotestring("say \"hi\"", crate::ported::zsh_h::QT_DOUBLE),
"\"say \\\"hi\\\"\""
);
}
#[test]
fn test_quotestring_dollars() {
assert_eq!(quotestring("hello", crate::ported::zsh_h::QT_DOLLARS), "$'hello'");
assert_eq!(
quotestring("line\nbreak", crate::ported::zsh_h::QT_DOLLARS),
"$'line\\nbreak'"
);
assert_eq!(
quotestring("tab\there", crate::ported::zsh_h::QT_DOLLARS),
"$'tab\\there'"
);
}
#[test]
fn test_quotestring_pattern() {
assert_eq!(quotestring("*.txt", crate::ported::zsh_h::QT_BACKSLASH_PATTERN), "\\*.txt");
assert_eq!(
quotestring("file[1]", crate::ported::zsh_h::QT_BACKSLASH_PATTERN),
"file\\[1\\]"
);
}
#[test]
fn test_quotetype_from_q_count() {
assert_eq!(qflag_quotetype(1), crate::ported::zsh_h::QT_BACKSLASH);
assert_eq!(qflag_quotetype(2), crate::ported::zsh_h::QT_SINGLE);
assert_eq!(qflag_quotetype(3), crate::ported::zsh_h::QT_DOUBLE);
assert_eq!(qflag_quotetype(4), crate::ported::zsh_h::QT_DOLLARS);
}
#[test]
fn test_tulower_tuupper() {
assert_eq!(tulower('A'), 'a');
assert_eq!(tuupper('a'), 'A');
assert_eq!(tulower('1'), '1');
}
#[test]
fn test_wordcount_ifs_default() {
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() {
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 mut buf = [0u8; 8];
let n = ucs4tomb('A' as u32, &mut buf);
assert!(n == 1);
assert_eq!(buf[0], b'A');
}
#[test]
fn test_is_mb_niceformat_plain_ascii() {
assert!(!is_mb_niceformat("hello world"));
}
#[test]
fn test_is_mb_niceformat_with_control_char() {
assert!(is_mb_niceformat("a\tb"));
assert!(is_mb_niceformat("a\x07b"));
}
#[test]
fn metafy_unmetafy_round_trips_for_ascii() {
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() {
assert_eq!(ztrlen(""), 0);
assert_eq!(ztrlen("a"), 1);
assert_eq!(ztrlen("hello"), 5);
}
#[test]
fn ztrlen_counts_meta_pair_as_one() {
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() {
use std::os::unix::io::AsRawFd;
let dir = tempfile::TempDir::new().unwrap();
let f = std::fs::File::create(dir.path().join("regular")).unwrap();
let fd = f.as_raw_fd();
let (changed, mode) = crate::ported::utils::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 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) = crate::ported::utils::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 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;
}
crate::ported::utils::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 (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() {
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() {
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() {
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 (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 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 t = std::time::Instant::now();
assert_eq!(timespec_diff_us(&t, &t), 0,
"c:2752 — identical Instants → 0");
}
#[test]
fn ucs4toutf8_encodes_canonical_lengths() {
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() {
assert_eq!(ucs4toutf8(0xFFFF_FFFE), None,
"c:6763 — values out of Unicode range return None");
assert_eq!(ucs4toutf8(0xD800), None,
"U+D800 is a surrogate, char::from_u32 rejects it");
}
#[test]
fn dquotedztrdup_default_path_wraps_whole_string() {
if crate::ported::zsh_h::isset(crate::ported::zsh_h::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() {
if crate::ported::zsh_h::isset(crate::ported::zsh_h::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() {
assert_eq!(quotedzputs(""), "''",
"c:6470-6475 — empty input → ''");
}
#[test]
fn quotedzputs_wraps_specials_in_single_quotes() {
crate::ported::utils::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() {
crate::ported::utils::inittyptab();
assert_eq!(quotedzputs("hello"), "hello",
"c:6512 — no specials, no wrap");
assert_eq!(quotedzputs("abc123"), "abc123");
}
#[test]
fn strucpy_appends_t_to_dest() {
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 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() {
crate::ported::utils::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() {
crate::ported::utils::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() {
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() {
assert_eq!(addunprintable('\0'), "\\0",
"c:6097-6098 — NUL → \\0");
}
#[test]
fn addunprintable_octal_fallback_for_unnamed_controls() {
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() {
crate::ported::utils::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() {
crate::ported::utils::inittyptab();
let bytes: Vec<u8> = vec![Meta as u8, 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() {
assert_eq!(sb_niceformat("hello"), "hello",
"c:5886 — nicechar_sel passes printable through");
assert_eq!(sb_niceformat(""), "");
assert_eq!(sb_niceformat("ABC012!?@"), "ABC012!?@");
}
#[test]
fn sb_niceformat_escapes_controls() {
assert_eq!(sb_niceformat("a\nb"), "a\\nb",
"c:5886 — newline → \\n");
assert_eq!(sb_niceformat("a\tb"), "a\\tb");
assert_eq!(sb_niceformat("\x01"), "^A",
"c:5886 — control char → ^X form");
assert_eq!(sb_niceformat("\x7f"), "^?");
}
#[test]
fn sb_niceformat_unmetafies_before_formatting() {
let bytes: Vec<u8> = vec![Meta as u8, 0x41u8];
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
assert_eq!(sb_niceformat(s), "a",
"c:5872 — unmetafy first: Meta+0x41 → 'a' → printable passthrough");
let bytes: Vec<u8> = vec![Meta as u8, 0x20u8];
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
let r = sb_niceformat(s);
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() {
assert!(is_sb_niceformat("\n"),
"newline is nice");
assert!(is_sb_niceformat("a\tb"));
assert!(is_sb_niceformat("\x01"));
assert!(!is_sb_niceformat("hello"));
assert!(!is_sb_niceformat(""));
}
#[test]
fn metacharlenconv_plain_ascii_returns_one_byte() {
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 bytes: Vec<u8> = vec![Meta as u8, 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 (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 (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() {
assert!(!is_mb_niceformat("hello"),
"c:5509 — printable ASCII needs no nice-format");
assert!(!is_mb_niceformat(""),
"c:5486 — empty string has no chars to flag");
assert!(!is_mb_niceformat("ABC012!?@"));
}
#[test]
fn is_mb_niceformat_true_for_strings_with_controls() {
assert!(is_mb_niceformat("a\nb"),
"c:5509 — newline is nice");
assert!(is_mb_niceformat("\t"));
assert!(is_mb_niceformat("\x01"),
"c:5509 — control char is nice");
assert!(is_mb_niceformat("\x7f"),
"c:5509 — DEL is nice");
}
#[test]
fn mb_niceformat_preserves_printable_wide_chars() {
assert_eq!(mb_niceformat("hello"), "hello",
"c:5407 — printable ASCII passes through");
assert_eq!(mb_niceformat("café"), "café",
"c:5407 — Latin-1 'é' must NOT byte-mask to \\M-X");
assert_eq!(mb_niceformat("字"), "字",
"c:5407 — CJK printable passes through");
assert_eq!(mb_niceformat("abcéxyz"), "abcéxyz");
}
#[test]
fn mb_niceformat_escapes_controls() {
assert_eq!(mb_niceformat("\n"), "\\n",
"c:5407 → wcs_nicechar c:625 — newline escapes");
assert_eq!(mb_niceformat("\t"), "\\t",
"c:5407 → wcs_nicechar c:628 — tab escapes");
assert_eq!(mb_niceformat("a\nb"), "a\\nb");
}
#[test]
fn metalen_returns_metafied_byte_count() {
assert_eq!(metalen("hello", 5), 5,
"c:4972 — ASCII: metafied bytes == unmetafied chars");
let bytes: Vec<u8> = vec![b'a', Meta as u8, 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 as u8, 0x41, Meta as u8, 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() {
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 (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() {
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 bytes: Vec<u8> = vec![Meta as u8, 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 as u8, 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 bytes: Vec<u8> = vec![Meta as u8];
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() {
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 meta_byte = Meta as u8;
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() {
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 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() {
assert!(!isident("1foo"));
assert!(!isident("99"));
assert!(!isident(""));
}
#[test]
fn isident_accepts_underscore_and_alpha_leading() {
assert!(isident("foo"));
assert!(isident("_foo"));
assert!(isident("Foo_BAR_42"));
assert!(isident("a"));
}
#[test]
fn isident_rejects_special_chars() {
assert!(!isident("foo bar"));
assert!(!isident("foo-bar"));
assert!(!isident("foo."));
assert!(!isident("a$b"));
}
#[test]
fn convbase_zero_renders_as_zero_literal() {
assert_eq!(convbase(0, 10), "0");
}
#[test]
fn convbase_uses_base_prefix_syntax_for_non_decimal() {
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() {
assert_eq!(convbase(-42, 10), "-42");
}
#[test]
fn slashsplit_filters_empty_segments() {
assert_eq!(slashsplit("/usr/local/bin"),
vec!["usr".to_string(), "local".to_string(), "bin".to_string()]);
assert_eq!(slashsplit("//foo"),
vec!["foo".to_string()],
"consecutive slashes filtered");
assert_eq!(slashsplit(""), Vec::<String>::new());
assert_eq!(slashsplit("/"), Vec::<String>::new());
}
#[test]
fn slashsplit_relative_path_no_trailing_empty() {
assert_eq!(slashsplit("a/b/"),
vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn equalsplit_returns_first_equals_split() {
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() {
assert_eq!(equalsplit("foo"), None);
assert_eq!(equalsplit(""), None);
}
#[test]
fn ztrcmp_deterministic_and_lexicographic() {
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() {
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 meta_byte = Meta as u8;
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() {
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() {
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() {
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 !crate::ported::zsh_h::isset(crate::ported::zsh_h::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() {
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() {
assert_eq!(zstrtoul_underscore("+42"), Some(42));
assert_eq!(zstrtoul_underscore("+0xff"), Some(255));
}
#[test]
fn nicechar_sel_passes_printable_ascii_unchanged() {
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() {
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() {
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() {
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() {
if crate::ported::zsh_h::isset(crate::ported::zsh_h::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() {
assert_eq!(wcs_nicechar_sel('a', false), "a", "ASCII printable");
assert_eq!(wcs_nicechar_sel('é', false), "é", "Latin-1 printable");
assert_eq!(wcs_nicechar_sel('字', false), "字", "CJK printable");
}
#[test]
fn wcs_nicechar_sel_escapes_newline_and_tab() {
assert_eq!(wcs_nicechar_sel('\n', false), "\\n");
assert_eq!(wcs_nicechar_sel('\t', false), "\\t");
}
#[test]
fn wcs_nicechar_sel_del_uses_caret_question() {
assert_eq!(wcs_nicechar_sel('\x7f', false), "^?");
assert_eq!(wcs_nicechar_sel('\x7f', true), "\\C-?");
}
#[test]
fn wcs_nicechar_sel_control_chars_use_caret_prefix() {
assert_eq!(wcs_nicechar_sel('\x01', false), "^A");
assert_eq!(wcs_nicechar_sel('\x07', false), "^G");
assert_eq!(wcs_nicechar_sel('\x1b', false), "^[");
assert_eq!(wcs_nicechar_sel('\x01', true), "\\C-A");
}
#[test]
fn wcs_nicechar_sel_large_nonprintable_uses_hex_escape() {
let nel = char::from_u32(0x85).unwrap();
let r = wcs_nicechar_sel(nel, false);
assert!(!r.is_empty(), "must emit something for U+0085");
let zwsp = char::from_u32(0x200B).unwrap();
let r = wcs_nicechar_sel(zwsp, false);
assert!(!r.is_empty());
}
#[test]
fn wcs_nicechar_matches_wcs_nicechar_sel_with_zero() {
for c in ['a', '\n', '\t', '\x7f', '\x01', 'é', '字'] {
assert_eq!(wcs_nicechar(c), wcs_nicechar_sel(c, false),
"c:711 — `wcs_nicechar(c)` must equal `wcs_nicechar_sel(c, 0)` for {:?}", c);
}
}
#[test]
#[cfg(unix)]
fn movefd_returns_minus_one_unchanged() {
assert_eq!(movefd(-1), -1, "fd=-1 must be returned unchanged");
}
#[test]
#[cfg(unix)]
fn movefd_returns_high_fd_unchanged() {
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_is_noop_stub() {
assert!(check_fd_table(-1), "stub returns true for any fd");
assert!(check_fd_table(0));
assert!(check_fd_table(99999));
}
#[test]
fn wcsitype_iword_reads_from_canonical_wordchars_global() {
if !isset(crate::ported::zsh_h::MULTIBYTE) { return; }
let saved = crate::ported::params::wordcharsgetfn();
crate::ported::params::wordcharssetfn("é".to_string());
crate::ported::params::wordcharssetfn(":".to_string());
assert!(wcsitype(':', IWORD as u32),
"c:4364 — wordchars membership through canonical global");
crate::ported::params::wordcharssetfn(saved);
}
#[cfg(unix)]
#[test]
fn addmodulefd_respects_fdt_parameter() {
use crate::ported::zsh_h::{FDT_EXTERNAL, FDT_MODULE};
let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
if fd < 0 { return; }
crate::ported::utils::addmodulefd(fd, FDT_EXTERNAL);
assert_eq!(crate::ported::utils::fdtable_get(fd), FDT_EXTERNAL,
"c:2095 — fdt parameter must be stored verbatim");
crate::ported::utils::addmodulefd(fd, FDT_MODULE);
assert_eq!(crate::ported::utils::fdtable_get(fd), FDT_MODULE,
"c:2095 — second call overwrites with new fdt");
unsafe { libc::close(fd); }
}
#[test]
fn addmodulefd_ignores_negative_fd() {
use crate::ported::zsh_h::FDT_MODULE;
crate::ported::utils::addmodulefd(-1, FDT_MODULE);
}
#[cfg(unix)]
#[test]
fn zcloselockfd_returns_minus_one_for_non_lock_fd() {
let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
if fd < 0 { return; }
let r = crate::ported::utils::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 fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
if fd < 0 { return; }
crate::ported::utils::addlockfd(fd, true);
let r = crate::ported::utils::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() {
use crate::ported::zsh_h::FDT_FLOCK;
let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
if fd < 0 { return; }
crate::ported::utils::fdtable_set(fd, FDT_FLOCK);
let max_fd = MAX_ZSH_FD.load(std::sync::atomic::Ordering::Relaxed);
assert!(max_fd >= fd,
"c:1982 — max_zsh_fd ({}) must be >= newly-set fd ({})",
max_fd, fd);
crate::ported::utils::fdtable_set(fd, crate::ported::zsh_h::FDT_UNUSED);
unsafe { libc::close(fd); }
}
#[cfg(unix)]
#[test]
fn addlockfd_selects_flock_category_per_cloexec_flag() {
use crate::ported::zsh_h::{FDT_FLOCK, FDT_FLOCK_EXEC};
let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
if fd < 0 { return; }
crate::ported::utils::addlockfd(fd, true);
assert_eq!(crate::ported::utils::fdtable_get(fd), FDT_FLOCK,
"c:2117 — cloexec=true → FDT_FLOCK");
crate::ported::utils::addlockfd(fd, false);
assert_eq!(crate::ported::utils::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() {
if !crate::ported::zsh_h::interact() {
return;
}
let name = "ZSHRS_TEST_PATHMAX_DIR";
let _ = crate::ported::hashnameddir::removenameddirnode(name);
let mut over: String = "/".to_string();
over.push_str(&"a".repeat(libc::PATH_MAX as usize));
crate::ported::utils::adduserdir(name, &over, 0, true);
let tab = crate::ported::hashnameddir::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 test_name = format!("zshrs_test_pathprog_{}", unsafe { libc::getpid() });
let path = std::path::PathBuf::from("/tmp").join(&test_name);
if std::fs::write(&path, b"plain content").is_err() {
return; }
let meta = std::fs::metadata(&path).unwrap();
assert_eq!(meta.permissions().mode() & 0o111, 0,
"test setup: must be non-executable");
let saved_path = crate::ported::params::getsparam("PATH");
crate::ported::params::assignsparam("PATH", "/tmp", 0);
let r = pathprog(&test_name);
let _ = std::fs::remove_file(&path);
if let Some(prev) = saved_path {
crate::ported::params::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 test_name = format!("zshrs_test_pathprog_dir_{}", unsafe { libc::getpid() });
let path = std::path::PathBuf::from("/tmp").join(&test_name);
if std::fs::create_dir(&path).is_err() {
return;
}
let saved_path = crate::ported::params::getsparam("PATH");
crate::ported::params::assignsparam("PATH", "/tmp", 0);
let r = pathprog(&test_name);
let _ = std::fs::remove_dir(&path);
if let Some(prev) = saved_path {
crate::ported::params::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() {
if !isset(crate::ported::zsh_h::MULTIBYTE) { return; }
let saved = crate::ported::params::ifsgetfn();
crate::ported::params::ifssetfn(":".to_string());
assert!(wcsitype(':', ISEP as u32),
"c:4367 — IFS membership through canonical global");
crate::ported::params::ifssetfn(saved);
}
#[test]
fn is_nicechar_highbit_is_nice_when_printeightbit_off() {
let c = char::from_u32(0xb5).unwrap();
if !crate::ported::zsh_h::isset(crate::ported::zsh_h::PRINTEIGHTBIT) {
assert!(is_nicechar(c),
"c:536 — high-bit byte 0x{:x} must be nice when PRINTEIGHTBIT off",
0xb5_u32);
}
}
}