use std::{
borrow::Cow,
env,
ffi::{CStr, CString, OsStr},
fmt,
io::{Read, Write},
marker::PhantomData,
os::{
fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd},
unix::ffi::OsStrExt,
},
time::Instant,
};
use bitflags::bitflags;
use data_encoding::HEXLOWER;
use dur::Duration;
use libseccomp::{ScmpAction, ScmpFilterContext, ScmpSyscall};
use memchr::memchr3;
use nix::{
errno::Errno,
fcntl::{open, OFlag},
libc::{_exit, c_char, size_t, ENOSYS, SIGCHLD, SIGKILL, SIGSYS},
mount::MsFlags,
sched::{unshare, CloneFlags},
sys::{
resource::Resource,
signal::{sigprocmask, SigSet, SigmaskHow, Signal},
stat::Mode,
wait::{Id, WaitPidFlag},
},
unistd::{chdir, Gid, Uid},
};
use crate::{
compat::{
pipe2_raw, set_dumpable, set_name, set_no_new_privs, set_pdeathsig, waitid, MFdFlags,
WaitStatus,
},
config::{
ALLOC_SYSCALLS, ENV_SKIP_SCMP, ESYD_SH, FUTEX_SYSCALLS, GETID_SYSCALLS, LANDLOCK_ABI,
VDSO_SYSCALLS, WORDEXP_SYSCALLS,
},
confine::{
confine_mdwe, confine_rlimit_zero, confine_scmp_madvise, confine_scmp_wx_all,
safe_drop_caps, secure_getenv, CLONE_NEWTIME,
},
cookie::safe_memfd_create,
debug,
err::err2no,
fd::{
close, fdclone, pidfd_send_signal, seal_memfd_all, set_cloexec, set_nonblock, SafeOwnedFd,
},
hash::SydHashSet,
landlock::RulesetStatus,
landlock_policy::LandlockPolicy,
log::contains_ascii_unprintable,
lookup::safe_copy_if_exists,
mount::{
api::MountAttrFlags,
util::{mount_bind, mount_fs, set_root_mount_propagation},
},
path::PATH_MAX,
proc::{proc_map_user, proc_open},
XPathBuf,
};
bitflags! {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WordExpFlags: i32 {
const WRDE_NOCMD = 1 << 2;
const WRDE_SHOWERR = 1 << 4;
const WRDE_UNDEF = 1 << 5;
}
}
impl Default for WordExpFlags {
fn default() -> Self {
Self::WRDE_NOCMD
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum WordExpError {
BadCharacter,
BadValue,
CommandSubstitution,
OutOfMemory,
Syntax,
SystemError(Errno),
SeccompError,
ProcessError(i32),
TimeoutError(u128),
}
pub const WRDE_NOSPACE: i32 = 1;
pub const WRDE_BADCHAR: i32 = 2;
pub const WRDE_BADVAL: i32 = 3;
pub const WRDE_CMDSUB: i32 = 4;
pub const WRDE_SYNTAX: i32 = 5;
pub const WRDE_SECCOMP: i32 = 127;
pub const WRDE_TIMEOUT: i32 = 126;
impl From<std::io::Error> for WordExpError {
fn from(io_err: std::io::Error) -> Self {
Self::SystemError(err2no(&io_err))
}
}
impl From<Errno> for WordExpError {
fn from(err: Errno) -> Self {
Self::SystemError(err)
}
}
impl From<i32> for WordExpError {
fn from(code: i32) -> Self {
if code > 128 {
return Self::SystemError(Errno::from_raw(code));
}
#[expect(clippy::arithmetic_side_effects)]
match code {
WRDE_BADCHAR => Self::BadCharacter,
WRDE_BADVAL => Self::BadValue,
WRDE_CMDSUB => Self::CommandSubstitution,
WRDE_NOSPACE => Self::OutOfMemory,
WRDE_SYNTAX => Self::Syntax,
WRDE_SECCOMP => Self::SeccompError,
_ => Self::SystemError(Errno::from_raw(code - 128)),
}
}
}
impl From<WordExpError> for i32 {
fn from(val: WordExpError) -> Self {
#[expect(clippy::arithmetic_side_effects)]
match val {
WordExpError::BadCharacter => WRDE_BADCHAR,
WordExpError::BadValue => WRDE_BADVAL,
WordExpError::CommandSubstitution => WRDE_CMDSUB,
WordExpError::OutOfMemory => WRDE_NOSPACE,
WordExpError::Syntax => WRDE_SYNTAX,
WordExpError::SeccompError => WRDE_SECCOMP,
WordExpError::ProcessError(sig) => 128 + sig,
WordExpError::SystemError(errno) => 128 + errno as i32,
WordExpError::TimeoutError(_) => WRDE_TIMEOUT,
}
}
}
impl fmt::Display for WordExpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WordExpError::SystemError(Errno::EINVAL) => write!(
f,
"environment expansion is not permitted, enable with config/expand"
),
WordExpError::CommandSubstitution => write!(
f,
"command substitution is not permitted, enable with config/expand_cmd"
),
WordExpError::BadValue => write!(f, "empty replacement is not permitted"),
WordExpError::BadCharacter => write!(
f,
"illegal occurrence of newline or one of |, &, ;, <, >, (, ), {{, }}"
),
WordExpError::OutOfMemory => write!(f, "out of memory"),
WordExpError::Syntax => write!(f, "shell syntax error"),
WordExpError::SeccompError => write!(f, "seccomp error: invalid system call"),
WordExpError::SystemError(e) => write!(f, "system error: {e}"),
WordExpError::ProcessError(sig) => {
let sig = Signal::try_from(*sig)
.map(|s| s.as_str())
.unwrap_or("SIGUNKNOWN");
write!(f, "process error: received signal {sig}")
}
WordExpError::TimeoutError(t) => {
let s = if *t > 1 { "s" } else { "" };
write!(f, "timeout error: runtime exceeded {t} second{s}")
}
}
}
}
#[repr(C)]
struct wordexp_t {
we_wordc: size_t,
we_wordv: *mut *mut c_char,
we_offs: size_t,
}
extern "C" {
fn wordexp(s: *const c_char, p: *mut wordexp_t, flags: i32) -> i32;
fn wordfree(p: *mut wordexp_t);
}
pub struct WordExp<'a> {
p: wordexp_t,
i: usize,
_m: PhantomData<&'a ()>,
}
impl Drop for WordExp<'_> {
fn drop(&mut self) {
unsafe { wordfree(std::ptr::addr_of_mut!(self.p)) };
}
}
impl<'a> Iterator for WordExp<'a> {
type Item = &'a OsStr;
fn next(&mut self) -> Option<Self::Item> {
if self.i >= self.p.we_wordc {
return None;
}
let off = isize::try_from(self.i).ok()?;
let ptr = unsafe { self.p.we_wordv.offset(off) };
if ptr.is_null() {
return None;
}
let ret = Some(OsStr::from_bytes(
unsafe { CStr::from_ptr(*ptr) }.to_bytes(),
));
if let Some(i) = self.i.checked_add(1) {
self.i = i;
}
ret
}
}
impl WordExp<'_> {
pub fn expand_word(s: &str, flags: WordExpFlags) -> Result<Self, i32> {
let c_s = CString::new(s).or(Err(WRDE_BADCHAR))?;
let mut p: wordexp_t = unsafe { std::mem::zeroed() };
let ret = unsafe { wordexp(c_s.as_ptr(), std::ptr::addr_of_mut!(p), flags.bits()) };
if ret != 0 {
return Err(ret);
}
Ok(Self {
p,
i: 0,
_m: PhantomData,
})
}
#[expect(clippy::cognitive_complexity)]
pub fn expand_full(input: &str, timeout: Duration) -> Result<Cow<'_, str>, WordExpError> {
if input.is_empty() || memchr3(b'$', b'`', b'(', input.as_bytes()).is_none() {
return Ok(Cow::Borrowed(input));
}
if timeout.is_zero() {
return Err(WordExpError::SystemError(Errno::EINVAL));
}
let mut fd = safe_memfd_create(
c"syd-wordexp",
MFdFlags::MFD_ALLOW_SEALING | MFdFlags::MFD_CLOEXEC,
)?;
debug!("ctx": "expand",
"msg": format!("created memory-file {} with close-on-exec flag set",
fd.as_raw_fd()));
fd.write_all(ESYD_SH.as_bytes())?;
fd.write_all(b"\n")?;
safe_copy_if_exists(&mut fd, "/etc/syd/init.sh")?;
fd.write_all(b"\n")?;
if let Some(home) = env::var_os("HOME").map(XPathBuf::from) {
safe_copy_if_exists(&mut fd, &home.join(b".config/syd/init.sh"))?;
fd.write_all(b"\n")?;
}
fd.write_all(b"eval set -- x ")?;
fd.write_all(input.as_bytes())?;
fd.write_all(b"\nshift\nprintf '%s ' \"$@\"\n")?;
seal_memfd_all(&fd)?;
debug!("ctx": "expand",
"msg": format!("sealed memory-file {} against grows, shrinks and writes",
fd.as_raw_fd()));
set_cloexec(&fd, false)?;
debug!("ctx": "expand",
"msg": format!("set close-on-exec flag to off for memory-file {}",
fd.as_raw_fd()));
let shell = format!("`. /proc/thread-self/fd/{}`", fd.as_raw_fd());
debug!("ctx": "expand",
"msg": format!("passing memory file {} to wordexp(3) with {} seconds timeout...",
fd.as_raw_fd(), timeout.as_secs()));
Ok(Cow::Owned(Self::expand(&shell, true, timeout)?.to_string()))
}
pub fn expand(
input: &str,
cmd_subs: bool,
timeout: Duration,
) -> Result<Cow<'_, str>, WordExpError> {
if input.is_empty() || memchr3(b'$', b'`', b'(', input.as_bytes()).is_none() {
return Ok(Cow::Borrowed(input));
}
if timeout.is_zero() {
return Err(WordExpError::SystemError(Errno::EINVAL));
}
let mut flags = WordExpFlags::WRDE_SHOWERR;
if !cmd_subs {
flags |= WordExpFlags::WRDE_NOCMD;
}
let (pipe_rd, pipe_wr) = pipe2_raw(OFlag::O_CLOEXEC)?;
let pipe_rd_ref = unsafe { BorrowedFd::borrow_raw(pipe_rd) };
set_nonblock(pipe_rd_ref, true)?;
let epoch = Instant::now();
let (pid_fd, _) = fdclone(
move || {
let _ = close(pipe_rd);
let mut pipe = unsafe { SafeOwnedFd::from_raw_fd(pipe_wr) };
let _ = set_name(c"syd_exp");
Self::confine();
debug!("ctx": "expand",
"msg": format!("calling wordexp(3), good luck!"));
for word in match Self::expand_word(input, flags) {
Ok(iter) => iter,
Err(err) =>
unsafe { _exit(err) },
} {
if word.is_empty() {
continue;
}
if let Err(ref error) = pipe.write_all(word.as_bytes()) {
let err = err2no(error) as i32;
#[expect(clippy::arithmetic_side_effects)]
unsafe {
_exit(128 + err)
};
}
if let Err(ref error) = pipe.write_all(b" ") {
let err = err2no(error) as i32;
#[expect(clippy::arithmetic_side_effects)]
unsafe {
_exit(128 + err)
};
}
}
unsafe { _exit(0) };
},
CloneFlags::empty(),
Some(SIGCHLD),
)?;
let _ = close(pipe_wr);
let mut pipe = unsafe { SafeOwnedFd::from_raw_fd(pipe_rd) };
let mut eof = false;
let mut sig = false;
let mut err = Errno::UnknownErrno;
let mut buf = [0u8; PATH_MAX];
let mut ret = Vec::new();
loop {
if !sig && (err as i32 != 0 || epoch.elapsed() >= timeout.into()) {
sig = true;
let _ = pidfd_send_signal(&pid_fd, SIGKILL);
} else if !eof {
match pipe.read(&mut buf) {
Ok(0) => {
eof = true;
}
Ok(n) => {
if ret.try_reserve(n).is_err() {
err = Errno::ENOMEM;
} else {
ret.extend(&buf[..n]);
}
continue;
}
Err(ref e) if matches!(err2no(e), Errno::EAGAIN | Errno::EINTR) => {
std::thread::sleep(Duration::from_millis(100).into());
continue;
}
Err(ref e) => {
err = err2no(e);
continue;
}
};
}
match waitid(
Id::PIDFd(pid_fd.as_fd()),
WaitPidFlag::WEXITED | WaitPidFlag::WNOHANG,
) {
Ok(WaitStatus::Exited(_, 0)) if eof => break,
Ok(WaitStatus::Exited(_, 0)) => {
let mut end = Vec::new();
if end.try_reserve(16).is_err() {
return Err(WordExpError::OutOfMemory);
}
if let Err(e) = set_nonblock(&pipe, false) {
return Err(WordExpError::SystemError(e));
}
match pipe.read_to_end(&mut end) {
Ok(0) => break,
Ok(n) => {
if ret.try_reserve(n).is_err() {
return Err(WordExpError::OutOfMemory);
}
ret.extend(&end[..n]);
break;
}
Err(ref e) => return Err(WordExpError::SystemError(err2no(e))),
}
}
Ok(WaitStatus::Exited(_, n)) => return Err(WordExpError::from(n)),
Ok(WaitStatus::Signaled(_, SIGSYS, _)) => return Err(WordExpError::SeccompError),
Ok(WaitStatus::Signaled(_, SIGKILL, _)) if err == Errno::ENOMEM => {
return Err(WordExpError::OutOfMemory)
}
Ok(WaitStatus::Signaled(_, SIGKILL, _)) if err as i32 != 0 => {
return Err(WordExpError::SystemError(err))
}
Ok(WaitStatus::Signaled(_, SIGKILL, _)) => {
return Err(WordExpError::TimeoutError(timeout.as_secs()))
}
Ok(WaitStatus::Signaled(_, sig, _)) => return Err(WordExpError::ProcessError(sig)),
_ => {}
};
}
if ret.is_empty() {
return Err(WordExpError::BadValue);
}
ret.pop();
let ret = match std::str::from_utf8(&ret) {
Ok(ret) => ret.to_string(),
Err(_) => return Ok(HEXLOWER.encode(&ret).into()),
};
if ret.is_empty() {
return Err(WordExpError::BadValue);
}
if contains_ascii_unprintable(ret.as_bytes()) {
Ok(HEXLOWER.encode(ret.as_bytes()).into())
} else {
Ok(ret.into())
}
}
#[expect(clippy::cognitive_complexity)]
#[expect(clippy::disallowed_methods)]
pub fn confine() {
if secure_getenv(ENV_SKIP_SCMP).is_some() {
return;
}
safe_drop_caps().expect("drop Linux capabilities(7)");
debug!("ctx": "expand", "msg": "dropped all Linux capabilities(7)");
set_no_new_privs().expect("set no-new-privs attribute");
debug!("ctx": "expand", "msg": "set no-new-privileges attribute");
match set_dumpable(false) {
Ok(_) => {
debug!("ctx": "expand",
"msg": "set process dumpable attribute to not-dumpable");
}
Err(errno) => {
debug!("ctx": "expand",
"msg": format!("failed to set process dumpable attribute attribute: {errno}"));
}
}
chdir(c"/proc/thread-self/fdinfo").expect("change to safe dir");
debug!("ctx": "expand",
"msg": "changed directory to /proc/thread-self/fdinfo");
let _ = Self::setup_namespaces(Uid::current(), Gid::current());
let mut path_ro = SydHashSet::default();
let mut path_rw = SydHashSet::default();
for ro in [
"/bin",
"/dev/null",
"/dev/random",
"/dev/urandom",
"/dev/zero",
"/lib",
"/lib64",
"/libexec",
"/opt",
"/sbin",
"/usr",
"/etc/ld.so.conf",
"/etc/ld.so.cache",
"/etc/ld.so.conf.d",
"/etc/ld-x86_64-pc-linux-musl.path",
"/etc/ld-musl-aarch64.path",
"/etc/ld-musl-aarch64.d",
] {
path_ro.insert(XPathBuf::from(ro));
}
path_rw.insert(XPathBuf::from("/dev/null"));
let policy = LandlockPolicy {
read_pathset: Some(path_ro.clone()),
readdir_pathset: Some(path_ro.clone()),
exec_pathset: Some(path_ro.clone()),
write_pathset: Some(path_rw.clone()),
truncate_pathset: Some(path_rw.clone()),
scoped_abs: true,
..Default::default()
};
let abi = *LANDLOCK_ABI as i32;
match policy.restrict_self(*LANDLOCK_ABI) {
Ok(status) => match status.ruleset {
RulesetStatus::FullyEnforced => {
debug!("ctx": "expand",
"msg": format!("Landlock ABI {abi} is fully enforced"),
"abi": abi);
}
RulesetStatus::PartiallyEnforced => {
debug!("ctx": "expand",
"msg": format!("Landlock ABI {abi} is partially enforced"),
"abi": abi);
}
RulesetStatus::NotEnforced => {
debug!("ctx": "expand",
"msg": format!("Landlock ABI {abi} is not enforced"),
"abi": abi);
}
},
Err(error) => {
debug!("ctx": "expand",
"msg": format!("Landlock ABI {abi} is unsupported: {error}"),
"abi": abi);
}
}
match confine_mdwe(false) {
Ok(_) => {
debug!("ctx": "expand",
"msg": "set Memory-Deny-Write-Execute attribute to deny W^X memory");
}
Err(Errno::EINVAL) => {
debug!("ctx": "expand",
"msg": "Memory-Deny-Write-Execute attribute requires Linux-6.3 or newer");
}
Err(Errno::EPERM) => {
debug!("ctx": "expand",
"msg": "Memory-Deny-Write-Execute attribute was set already");
}
Err(Errno::ENOTSUP) => {
debug!("ctx": "expand",
"msg": "Memory-Deny-Write-Execute attribute isn't supported on this architecture");
}
Err(errno) => {
debug!("ctx": "expand",
"msg": format!("failed to set Memory-Deny-Write-Execute attribute: {errno}"));
}
}
match confine_scmp_wx_all() {
Ok(_) => {
debug!("ctx": "expand",
"msg": "confined W^X memory syscalls with seccomp");
}
Err(error) => {
debug!("ctx": "expand",
"msg": format!("failed to confine W^X memory syscalls with seccomp: {error}"));
}
}
confine_rlimit_zero(&[
Resource::RLIMIT_CORE,
Resource::RLIMIT_FSIZE,
Resource::RLIMIT_LOCKS,
Resource::RLIMIT_MEMLOCK,
Resource::RLIMIT_MSGQUEUE,
])
.expect("set resource limit");
Self::confine_seccomp();
}
#[expect(clippy::disallowed_methods)]
fn confine_seccomp() {
let mut filter = ScmpFilterContext::new(ScmpAction::Errno(ENOSYS)).expect("create filter");
filter.set_ctl_nnp(true).expect("enforce no-new-privs");
filter
.set_act_badarch(ScmpAction::Errno(ENOSYS))
.expect("set bad architecture action");
let _ = filter.set_ctl_optimize(2);
confine_scmp_madvise(&mut filter).expect("filter madvise");
for sysname in WORDEXP_SYSCALLS
.iter()
.chain(ALLOC_SYSCALLS)
.chain(FUTEX_SYSCALLS)
.chain(GETID_SYSCALLS)
.chain(VDSO_SYSCALLS)
{
if let Ok(syscall) = ScmpSyscall::from_name(sysname) {
filter
.add_rule(ScmpAction::Allow, syscall)
.expect("filter syscall");
}
}
filter.load().expect("load filter");
debug!("ctx": "expand",
"msg": "loaded seccomp filter");
}
#[expect(clippy::disallowed_methods)]
fn setup_namespaces(uid: Uid, gid: Gid) -> Result<(), Errno> {
unshare(
CloneFlags::CLONE_NEWUSER
| CloneFlags::CLONE_NEWCGROUP
| CloneFlags::CLONE_NEWIPC
| CloneFlags::CLONE_NEWNET
| CloneFlags::CLONE_NEWNS
| CloneFlags::CLONE_NEWPID
| CloneFlags::CLONE_NEWUTS
| CLONE_NEWTIME,
)?;
debug!("ctx": "expand",
"msg": "created and entered into new user, mount, pid, network, cgroup, ipc, uts, and time namespaces");
proc_map_user(proc_open(None)?, uid, gid, false )?;
let mut flags = MountAttrFlags::MOUNT_ATTR_RDONLY
| MountAttrFlags::MOUNT_ATTR_NOSUID
| MountAttrFlags::MOUNT_ATTR_NODEV
| MountAttrFlags::MOUNT_ATTR_NOSYMFOLLOW;
set_root_mount_propagation(MsFlags::MS_PRIVATE)?;
debug!("ctx": "expand",
"msg": "set mount propagation to private in new mount namespace");
open(
"/",
OFlag::O_CLOEXEC | OFlag::O_PATH | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW,
Mode::empty(),
)
.and_then(|root| mount_bind(&root, &root, flags))?;
debug!("ctx": "expand",
"msg": "remounted root with readonly, nosuid, nodev, and nosymfollow options in new mount namespace");
flags.remove(MountAttrFlags::MOUNT_ATTR_NOSYMFOLLOW);
flags.insert(MountAttrFlags::MOUNT_ATTR_NOEXEC);
Self::mount_proc(flags);
Ok(())
}
#[expect(clippy::cognitive_complexity)]
#[expect(clippy::disallowed_methods)]
fn mount_proc(flags: MountAttrFlags) {
fdclone(
move || {
debug!("ctx": "expand",
"msg": "started init process in new pid namespace");
if set_pdeathsig(Some(Signal::SIGKILL)).is_err() {
unsafe { _exit(0) };
}
debug!("ctx": "expand",
"msg": "set parent-death signal to SIGKILL for the init process");
sigprocmask(SigmaskHow::SIG_BLOCK, Some(&SigSet::all()), None)
.expect("block signals");
match open(
"/proc",
OFlag::O_PATH | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC,
Mode::empty(),
)
.and_then(|proc| {
mount_fs(
OsStr::new("proc"),
proc,
flags,
Some("hidepid=4,subset=pid"),
)
}) {
Ok(_) => {
debug!("ctx": "expand",
"msg": "mounted proc with hidepid=4,subset=pid in new mount namespace");
}
Err(errno) => {
debug!("ctx": "expand",
"msg": format!("failed to mount private procfs: {errno}"));
}
};
std::thread::sleep(std::time::Duration::MAX);
unreachable!();
},
CloneFlags::CLONE_FILES,
Some(SIGCHLD),
)
.map(drop)
.expect("spawn pid1");
}
}
#[cfg(test)]
mod tests {
use nix::errno::Errno;
use super::*;
#[test]
fn test_wordexpflags_1() {
assert_eq!(WordExpFlags::default(), WordExpFlags::WRDE_NOCMD);
}
#[test]
fn test_wordexperror_2() {
assert_eq!(WordExpError::from(WRDE_NOSPACE), WordExpError::OutOfMemory);
}
#[test]
fn test_wordexperror_3() {
assert_eq!(WordExpError::from(WRDE_BADCHAR), WordExpError::BadCharacter);
}
#[test]
fn test_wordexperror_4() {
assert_eq!(WordExpError::from(WRDE_BADVAL), WordExpError::BadValue);
}
#[test]
fn test_wordexperror_5() {
assert_eq!(
WordExpError::from(WRDE_CMDSUB),
WordExpError::CommandSubstitution
);
}
#[test]
fn test_wordexperror_6() {
assert_eq!(WordExpError::from(WRDE_SYNTAX), WordExpError::Syntax);
}
#[test]
fn test_wordexperror_7() {
assert_eq!(WordExpError::from(WRDE_SECCOMP), WordExpError::SeccompError);
}
#[test]
fn test_wordexperror_8() {
assert_eq!(
WordExpError::from(200),
WordExpError::SystemError(Errno::from_raw(200))
);
}
#[test]
fn test_wordexperror_9() {
let unknown = 10;
assert_eq!(
WordExpError::from(unknown),
WordExpError::SystemError(Errno::from_raw(unknown - 128))
);
}
#[test]
fn test_wordexperror_10() {
assert_eq!(i32::from(WordExpError::BadCharacter), WRDE_BADCHAR);
}
#[test]
fn test_wordexperror_11() {
assert_eq!(i32::from(WordExpError::BadValue), WRDE_BADVAL);
}
#[test]
fn test_wordexperror_12() {
assert_eq!(i32::from(WordExpError::CommandSubstitution), WRDE_CMDSUB);
}
#[test]
fn test_wordexperror_13() {
assert_eq!(i32::from(WordExpError::OutOfMemory), WRDE_NOSPACE);
}
#[test]
fn test_wordexperror_14() {
assert_eq!(i32::from(WordExpError::Syntax), WRDE_SYNTAX);
}
#[test]
fn test_wordexperror_15() {
assert_eq!(i32::from(WordExpError::SeccompError), WRDE_SECCOMP);
}
#[test]
fn test_wordexperror_16() {
assert_eq!(i32::from(WordExpError::TimeoutError(5)), WRDE_TIMEOUT);
}
#[test]
fn test_wordexperror_17() {
assert_eq!(i32::from(WordExpError::ProcessError(9)), 128 + 9);
}
#[test]
fn test_wordexperror_18() {
assert_eq!(
i32::from(WordExpError::SystemError(Errno::ENOENT)),
128 + Errno::ENOENT as i32
);
}
#[test]
fn test_wordexperror_19() {
assert!(WordExpError::OutOfMemory
.to_string()
.contains("out of memory"));
}
#[test]
fn test_wordexperror_20() {
assert!(WordExpError::BadCharacter.to_string().contains("illegal"));
}
#[test]
fn test_wordexperror_21() {
assert!(WordExpError::BadValue
.to_string()
.contains("empty replacement"));
}
#[test]
fn test_wordexperror_22() {
assert!(WordExpError::CommandSubstitution
.to_string()
.contains("command substitution"));
}
#[test]
fn test_wordexperror_23() {
assert!(WordExpError::Syntax.to_string().contains("syntax"));
}
#[test]
fn test_wordexperror_24() {
assert!(WordExpError::SeccompError.to_string().contains("seccomp"));
}
#[test]
fn test_wordexperror_25() {
assert!(WordExpError::SystemError(Errno::EINVAL)
.to_string()
.contains("environment expansion"));
}
#[test]
fn test_wordexperror_26() {
assert!(WordExpError::ProcessError(9).to_string().contains("signal"));
}
#[test]
fn test_wordexperror_27() {
assert!(WordExpError::TimeoutError(3)
.to_string()
.contains("timeout"));
}
#[test]
fn test_wordexperror_28() {
assert!(WordExpError::TimeoutError(1)
.to_string()
.contains("1 second"));
}
#[test]
fn test_wordexperror_29() {
let err = WordExpError::from(Errno::EPERM);
assert_eq!(err, WordExpError::SystemError(Errno::EPERM));
}
#[test]
fn test_wordexperror_30() {
let io_err = std::io::Error::from(std::io::ErrorKind::PermissionDenied);
let err = WordExpError::from(io_err);
assert!(matches!(err, WordExpError::SystemError(_)));
}
#[test]
fn test_wordexpand_1() {
let result = WordExp::expand("", false, Duration::from_secs(1));
assert!(matches!(result, Ok(ref s) if s.as_ref() == ""));
}
#[test]
fn test_wordexpand_2() {
let result = WordExp::expand("hello", false, Duration::from_secs(1));
assert!(matches!(result, Ok(ref s) if s.as_ref() == "hello"));
}
#[test]
fn test_wordexpand_3() {
let result = WordExp::expand("$HOME", false, Duration::from_secs(0));
assert_eq!(result, Err(WordExpError::SystemError(Errno::EINVAL)));
}
#[test]
fn test_wordexpand_4() {
let result = WordExp::expand_full("", Duration::from_secs(1));
assert!(matches!(result, Ok(ref s) if s.as_ref() == ""));
}
#[test]
fn test_wordexpand_5() {
let result = WordExp::expand_full("hello world", Duration::from_secs(1));
assert!(matches!(result, Ok(ref s) if s.as_ref() == "hello world"));
}
#[test]
fn test_wordexpand_6() {
let result = WordExp::expand_full("$HOME", Duration::from_secs(0));
assert_eq!(result, Err(WordExpError::SystemError(Errno::EINVAL)));
}
#[test]
fn test_wordexpand_7() {
let result = WordExp::expand_word("hello", WordExpFlags::WRDE_NOCMD);
assert!(result.is_ok());
let words: Vec<_> = result.unwrap().collect();
assert_eq!(words.len(), 1);
}
#[test]
fn test_wordexpand_8() {
let result = WordExp::expand_word("hello world", WordExpFlags::WRDE_NOCMD);
assert!(result.is_ok());
let words: Vec<_> = result.unwrap().collect();
assert_eq!(words.len(), 2);
}
#[test]
fn test_wordexpand_9() {
let result = WordExp::expand_word("", WordExpFlags::WRDE_NOCMD);
assert!(result.is_ok());
let words: Vec<_> = result.unwrap().collect();
assert_eq!(words.len(), 0);
}
}