use async_trait::async_trait;
use super::{Builtin, Context, resolve_path};
use crate::error::Result;
use crate::interpreter::ExecResult;
pub struct Mkfifo;
fn parse_mode(s: &str) -> Option<u32> {
let trimmed = s.trim_start_matches('0');
let trimmed = if trimmed.is_empty() { "0" } else { trimmed };
u32::from_str_radix(trimmed, 8).ok()
}
#[async_trait]
impl Builtin for Mkfifo {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
if let Some(r) = super::check_help_version(
ctx.args,
"Usage: mkfifo [OPTION]... NAME...\nCreate named pipes (FIFOs) with the given NAMEs.\n\n -m MODE\tset file permission bits to MODE (default: 0666)\n --help\tdisplay this help and exit\n --version\toutput version information and exit\n",
Some("mkfifo (bashkit) 0.1"),
) {
return Ok(r);
}
if ctx.args.is_empty() {
return Ok(ExecResult::err("mkfifo: missing operand\n".to_string(), 1));
}
let mut paths = Vec::new();
let mut mode: u32 = 0o666;
let mut i = 0;
while i < ctx.args.len() {
let arg = &ctx.args[i];
if arg == "-m" {
if i + 1 >= ctx.args.len() {
return Ok(ExecResult::err(
"mkfifo: option requires an argument -- 'm'\n".to_string(),
1,
));
}
if let Some(m) = parse_mode(&ctx.args[i + 1]) {
mode = m;
} else {
return Ok(ExecResult::err(
format!("mkfifo: invalid mode '{}'\n", ctx.args[i + 1]),
1,
));
}
i += 2;
} else if let Some(mode_str) = arg.strip_prefix("-m") {
if let Some(m) = parse_mode(mode_str) {
mode = m;
} else {
return Ok(ExecResult::err(
format!("mkfifo: invalid mode '{}'\n", mode_str),
1,
));
}
i += 1;
} else if arg.starts_with('-') && arg != "-" {
return Ok(ExecResult::err(
format!("mkfifo: invalid option -- '{}'\n", &arg[1..]),
1,
));
} else {
paths.push(arg.clone());
i += 1;
}
}
if paths.is_empty() {
return Ok(ExecResult::err("mkfifo: missing operand\n".to_string(), 1));
}
let mut stderr = String::new();
let mut failed = false;
for name in &paths {
let path = resolve_path(ctx.cwd, name);
if let Err(e) = ctx.fs.mkfifo(&path, mode).await {
let msg = e.to_string();
if msg.contains("lready") || msg.contains("exists") {
stderr.push_str(&format!(
"mkfifo: cannot create fifo '{}': File exists\n",
name
));
} else if msg.contains("ot found") || msg.contains("o such") {
stderr.push_str(&format!(
"mkfifo: cannot create fifo '{}': No such file or directory\n",
name
));
} else {
stderr.push_str(&format!("mkfifo: cannot create fifo '{}': {}\n", name, e));
}
failed = true;
}
}
if failed {
Ok(ExecResult::err(stderr, 1))
} else {
Ok(ExecResult::ok(String::new()))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fs::{FileSystem, InMemoryFs};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
async fn run_mkfifo(args: &[&str]) -> ExecResult {
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let env = HashMap::new();
let mut variables = HashMap::new();
let mut cwd = PathBuf::from("/");
let fs = Arc::new(InMemoryFs::new());
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs, None);
Mkfifo.execute(ctx).await.unwrap()
}
async fn run_mkfifo_with_fs(args: &[&str], fs: Arc<InMemoryFs>) -> ExecResult {
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let env = HashMap::new();
let mut variables = HashMap::new();
let mut cwd = PathBuf::from("/");
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs, None);
Mkfifo.execute(ctx).await.unwrap()
}
#[tokio::test]
async fn mkfifo_creates_fifo() {
let fs = Arc::new(InMemoryFs::new());
let result = run_mkfifo_with_fs(&["mypipe"], fs.clone()).await;
assert_eq!(result.exit_code, 0);
assert!(fs.exists(Path::new("/mypipe")).await.unwrap());
let meta = fs.stat(Path::new("/mypipe")).await.unwrap();
assert!(meta.file_type.is_fifo());
}
#[tokio::test]
async fn mkfifo_multiple_paths() {
let fs = Arc::new(InMemoryFs::new());
let result = run_mkfifo_with_fs(&["pipe1", "pipe2", "pipe3"], fs.clone()).await;
assert_eq!(result.exit_code, 0);
assert!(fs.exists(Path::new("/pipe1")).await.unwrap());
assert!(fs.exists(Path::new("/pipe2")).await.unwrap());
assert!(fs.exists(Path::new("/pipe3")).await.unwrap());
}
#[tokio::test]
async fn mkfifo_existing_file_error() {
let fs = Arc::new(InMemoryFs::new());
fs.write_file(Path::new("/existing"), b"data")
.await
.unwrap();
let result = run_mkfifo_with_fs(&["existing"], fs).await;
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("File exists"));
}
#[tokio::test]
async fn mkfifo_missing_operand() {
let result = run_mkfifo(&[]).await;
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("missing operand"));
}
#[tokio::test]
async fn mkfifo_mode_flag_accepted() {
let fs = Arc::new(InMemoryFs::new());
let result = run_mkfifo_with_fs(&["-m", "0644", "mypipe"], fs.clone()).await;
assert_eq!(result.exit_code, 0);
let meta = fs.stat(Path::new("/mypipe")).await.unwrap();
assert_eq!(meta.mode, 0o644);
}
#[tokio::test]
async fn mkfifo_mode_flag_combined() {
let fs = Arc::new(InMemoryFs::new());
let result = run_mkfifo_with_fs(&["-m0755", "mypipe"], fs.clone()).await;
assert_eq!(result.exit_code, 0);
let meta = fs.stat(Path::new("/mypipe")).await.unwrap();
assert_eq!(meta.mode, 0o755);
}
#[tokio::test]
async fn mkfifo_mode_missing_arg() {
let result = run_mkfifo(&["-m"]).await;
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("option requires an argument"));
}
#[tokio::test]
async fn mkfifo_nonexistent_parent() {
let fs = Arc::new(InMemoryFs::new());
let result = run_mkfifo_with_fs(&["/no/such/dir/pipe"], fs).await;
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("No such file or directory"));
}
#[tokio::test]
async fn mkfifo_default_mode() {
let fs = Arc::new(InMemoryFs::new());
let result = run_mkfifo_with_fs(&["mypipe"], fs.clone()).await;
assert_eq!(result.exit_code, 0);
let meta = fs.stat(Path::new("/mypipe")).await.unwrap();
assert_eq!(meta.mode, 0o666);
}
}