use std::fs::{File, OpenOptions};
use crate::bun_shell::builtin::{Builtin, BuiltinIn};
use crate::bun_shell::io::{Reader, ShellSysError, Which};
use crate::bun_shell::node_path;
const EPERM: i32 = 1;
#[derive(Debug, PartialEq, Eq)]
enum Parsed {
Start(Option<usize>),
Illegal(Vec<u8>),
Unsupported(String),
}
fn parse_opts(args: &[String]) -> Parsed {
let Some(arg) = args.first() else {
return Parsed::Start(None);
};
if !arg.starts_with('-') {
return Parsed::Start(Some(0));
}
if arg.len() == 1 {
return Parsed::Illegal(b"-".to_vec());
}
let small = &arg.as_bytes()[1..];
let ch = small[0];
if b"bestuvn".contains(&ch) {
return Parsed::Unsupported(format!("-{}", ch as char));
}
Parsed::Illegal(small[1..].to_vec())
}
async fn fail(b: &mut Builtin<'_>, msg: &[u8], exit_code: i32) -> i32 {
if b.needs_io(Which::Stderr) {
return match b.write(Which::Stderr, msg).await {
Ok(()) => exit_code,
Err(e) => e.errno,
};
}
let _ = b.write_no_io(Which::Stderr, msg);
exit_code
}
async fn fail_opts(b: &mut Builtin<'_>, parsed: Parsed) -> i32 {
let msg = match parsed {
Parsed::Unsupported(opt) => b
.fmt_err(&format!(
"unsupported option, please open a GitHub issue -- {opt}\n"
))
.into_bytes(),
Parsed::Illegal(rest) => {
let mut msg = b.fmt_err("illegal option -- ").into_bytes();
msg.extend_from_slice(&rest);
msg.push(b'\n');
msg
}
Parsed::Start(_) => unreachable!("fail_opts called with operands"),
};
fail(b, &msg, 1).await
}
#[cfg(target_os = "linux")]
const UNPOLLABLE_MEM_MINORS: [u64; 5] = [1, 3, 4, 5, 7];
#[cfg(target_os = "linux")]
fn unpollable(reader: &Reader) -> bool {
use crate::bun_shell::io::ReaderSource;
use std::os::fd::AsFd;
use std::os::unix::fs::{FileTypeExt, MetadataExt};
let meta = match reader.source() {
ReaderSource::File(f) => f.metadata(),
ReaderSource::Stdin => std::io::stdin()
.as_fd()
.try_clone_to_owned()
.and_then(|fd| File::from(fd).metadata()),
ReaderSource::Channel(_) => return false,
};
let Ok(st) = meta else {
return false;
};
let ft = st.file_type();
if ft.is_file() || ft.is_dir() || ft.is_block_device() {
return true;
}
if ft.is_char_device() {
let rdev = st.rdev();
let major = (rdev >> 8) & 0xfff;
let minor = (rdev & 0xff) | ((rdev >> 12) & 0xffff_ff00);
return major == 1 && UNPOLLABLE_MEM_MINORS.contains(&minor);
}
false
}
#[cfg(not(target_os = "linux"))]
fn unpollable(_reader: &Reader) -> bool {
false
}
async fn pump(b: &mut Builtin<'_>, reader: &Reader) -> i32 {
if unpollable(reader) {
return EPERM;
}
let stdout_io = b.needs_io(Which::Stdout);
loop {
match reader.read_chunk().await {
Ok(None) => return 0,
Ok(Some(chunk)) => {
if stdout_io {
if let Err(e) = b.write(Which::Stdout, &chunk).await {
return e.errno;
}
} else {
let _ = b.write_no_io(Which::Stdout, &chunk);
}
}
Err(e) => return e.errno,
}
}
}
async fn cat_stdin(b: &mut Builtin<'_>) -> i32 {
if let BuiltinIn::Fd(reader) = &b.stdin {
let reader = reader.clone();
return pump(b, &reader).await;
}
let buf = b.read_stdin_no_io();
if b.needs_io(Which::Stdout) {
return match b.write(Which::Stdout, &buf).await {
Ok(()) => 0,
Err(e) => e.errno,
};
}
let _ = b.write_no_io(Which::Stdout, &buf);
0
}
fn open_read(p: &str) -> std::io::Result<File> {
let mut opts = OpenOptions::new();
opts.read(true);
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
opts.custom_flags(FILE_FLAG_BACKUP_SEMANTICS);
}
opts.open(p)
}
pub(crate) async fn cat(b: &mut Builtin<'_>) -> i32 {
let start = match parse_opts(&b.args) {
Parsed::Start(None) => return cat_stdin(b).await,
Parsed::Start(Some(start)) => start,
parsed => return fail_opts(b, parsed).await,
};
let cwd = b.cwd().to_string();
let operands = b.args[start..].to_vec();
for arg in &operands {
let opened = if arg.is_empty() {
Err(ShellSysError::new("ENOENT").with_syscall("open"))
} else {
let p = if node_path::is_absolute(arg) {
arg.clone()
} else {
format!("{}/{arg}", cwd.strip_suffix('/').unwrap_or(&cwd))
};
open_read(&p).map_err(|e| ShellSysError::from_io(&e, arg.as_str()).with_syscall("open"))
};
let file = match opened {
Ok(file) => file,
Err(err) => {
let msg = b.task_error_to_string(&err);
return fail(b, msg.as_bytes(), 1).await;
}
};
let code = pump(b, &Reader::file(file)).await;
if code != 0 {
return code;
}
}
0
}
#[cfg(test)]
#[path = "cat/tests.rs"]
mod tests;