use crate::error::{Error, Result};
use crate::runtime::{Runtime, Value};
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub(crate) fn chdir(rt: &mut Runtime, path: &str) -> Result<Value> {
rt.require_unsandboxed_io()?;
rt.clear_errno();
match std::env::set_current_dir(path) {
Ok(()) => Ok(Value::Num(0.0)),
Err(e) => {
rt.set_errno_io(&e);
Ok(Value::Num(-1.0))
}
}
}
pub(crate) fn stat(rt: &mut Runtime, path: &str, arr_name: &str) -> Result<Value> {
rt.require_unsandboxed_io()?;
rt.clear_errno();
let meta = match fs::metadata(path) {
Ok(m) => m,
Err(e) => {
rt.set_errno_io(&e);
return Ok(Value::Num(-1.0));
}
};
rt.array_delete(arr_name, None);
let file_type = if meta.is_dir() {
"directory"
} else if meta.is_symlink() {
"symlink"
} else if meta.is_file() {
"file"
} else {
"other"
};
rt.array_set(arr_name, "type".into(), Value::Str(file_type.into()));
rt.array_set(arr_name, "size".into(), Value::Num(meta.len() as f64));
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
rt.array_set(arr_name, "dev".into(), Value::Num(meta.dev() as f64));
rt.array_set(arr_name, "ino".into(), Value::Num(meta.ino() as f64));
rt.array_set(arr_name, "mode".into(), Value::Num(meta.mode() as f64));
rt.array_set(arr_name, "nlink".into(), Value::Num(meta.nlink() as f64));
rt.array_set(arr_name, "uid".into(), Value::Num(meta.uid() as f64));
rt.array_set(arr_name, "gid".into(), Value::Num(meta.gid() as f64));
rt.array_set(arr_name, "rdev".into(), Value::Num(meta.rdev() as f64));
rt.array_set(
arr_name,
"blksize".into(),
Value::Num(meta.blksize() as f64),
);
rt.array_set(arr_name, "blocks".into(), Value::Num(meta.blocks() as f64));
rt.array_set(arr_name, "atime".into(), Value::Num(meta.atime() as f64));
rt.array_set(arr_name, "mtime".into(), Value::Num(meta.mtime() as f64));
rt.array_set(arr_name, "ctime".into(), Value::Num(meta.ctime() as f64));
}
#[cfg(not(unix))]
{
rt.array_set(arr_name, "dev".into(), Value::Num(0.0));
rt.array_set(arr_name, "ino".into(), Value::Num(0.0));
rt.array_set(arr_name, "mode".into(), Value::Num(0.0));
rt.array_set(arr_name, "nlink".into(), Value::Num(1.0));
rt.array_set(arr_name, "uid".into(), Value::Num(0.0));
rt.array_set(arr_name, "gid".into(), Value::Num(0.0));
rt.array_set(arr_name, "rdev".into(), Value::Num(0.0));
rt.array_set(arr_name, "blksize".into(), Value::Num(0.0));
rt.array_set(arr_name, "blocks".into(), Value::Num(0.0));
if let Ok(t) = meta.accessed() {
rt.array_set(
arr_name,
"atime".into(),
Value::Num(
t.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0),
),
);
}
if let Ok(t) = meta.modified() {
rt.array_set(
arr_name,
"mtime".into(),
Value::Num(
t.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0),
),
);
}
}
Ok(Value::Num(0.0))
}
pub(crate) fn statvfs(rt: &mut Runtime, path: &str, arr_name: &str) -> Result<Value> {
rt.require_unsandboxed_io()?;
rt.clear_errno();
#[cfg(unix)]
{
use std::ffi::CString;
use std::mem::MaybeUninit;
let c = CString::new(path).map_err(|_| Error::Runtime("statvfs: path".into()))?;
let mut v: MaybeUninit<libc::statvfs> = MaybeUninit::uninit();
let r = unsafe { libc::statvfs(c.as_ptr(), v.as_mut_ptr()) };
if r != 0 {
let e = std::io::Error::last_os_error();
rt.set_errno_io(&e);
return Ok(Value::Num(-1.0));
}
let v = unsafe { v.assume_init() };
rt.array_delete(arr_name, None);
rt.array_set(arr_name, "f_bsize".into(), Value::Num(v.f_bsize as f64));
rt.array_set(arr_name, "f_frsize".into(), Value::Num(v.f_frsize as f64));
rt.array_set(arr_name, "f_blocks".into(), Value::Num(v.f_blocks as f64));
rt.array_set(arr_name, "f_bfree".into(), Value::Num(v.f_bfree as f64));
rt.array_set(arr_name, "f_bavail".into(), Value::Num(v.f_bavail as f64));
rt.array_set(arr_name, "f_files".into(), Value::Num(v.f_files as f64));
rt.array_set(arr_name, "f_ffree".into(), Value::Num(v.f_ffree as f64));
rt.array_set(arr_name, "f_favail".into(), Value::Num(v.f_favail as f64));
rt.array_set(arr_name, "f_fsid".into(), Value::Num(0.0));
rt.array_set(arr_name, "f_flag".into(), Value::Num(v.f_flag as f64));
rt.array_set(arr_name, "f_namemax".into(), Value::Num(v.f_namemax as f64));
Ok(Value::Num(0.0))
}
#[cfg(not(unix))]
{
let _ = (path, arr_name);
rt.set_errno_str("statvfs: not supported on this platform");
Ok(Value::Num(-1.0))
}
}
pub(crate) fn fts(rt: &mut Runtime, root: &str, arr_name: &str) -> Result<Value> {
rt.require_unsandboxed_io()?;
rt.clear_errno();
let root_path = Path::new(root);
if !root_path.exists() {
rt.set_errno_str("fts: path does not exist");
return Ok(Value::Num(-1.0));
}
let mut paths: Vec<String> = Vec::new();
let walker = walkdir::WalkDir::new(root_path).follow_links(false);
for e in walker.into_iter().filter_map(|e| e.ok()) {
paths.push(e.path().to_string_lossy().into_owned());
}
paths.sort();
rt.split_into_array(arr_name, &paths);
Ok(Value::Num(paths.len() as f64))
}
pub(crate) fn gettimeofday(rt: &mut Runtime, arr_name: &str) -> Result<Value> {
rt.clear_errno();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO);
rt.array_delete(arr_name, None);
rt.array_set(arr_name, "sec".into(), Value::Num(now.as_secs_f64()));
rt.array_set(
arr_name,
"usec".into(),
Value::Num(now.subsec_micros() as f64),
);
Ok(Value::Num(0.0))
}
pub(crate) fn sleep_secs(_rt: &mut Runtime, sec: f64) -> Result<Value> {
if sec < 0.0 {
return Err(Error::Runtime("sleep: negative duration".into()));
}
std::thread::sleep(Duration::from_secs_f64(sec));
Ok(Value::Num(0.0))
}
pub(crate) fn ord(_rt: &mut Runtime, s: &str) -> Result<Value> {
let n = s.chars().next().map(|c| c as u32).unwrap_or(0);
Ok(Value::Num(f64::from(n)))
}
pub(crate) fn chr(_rt: &mut Runtime, n: f64) -> Result<Value> {
let u = n as u32;
let s = char::from_u32(u).map(|c| c.to_string()).unwrap_or_default();
Ok(Value::Str(s))
}
pub(crate) fn readfile(rt: &mut Runtime, path: &str) -> Result<Value> {
rt.require_unsandboxed_io()?;
rt.clear_errno();
match fs::read_to_string(path) {
Ok(s) => Ok(Value::Str(s)),
Err(e) => {
rt.set_errno_io(&e);
Ok(Value::Str(String::new()))
}
}
}
pub(crate) fn revoutput(_rt: &mut Runtime, s: &str) -> Result<Value> {
Ok(Value::Str(s.chars().rev().collect()))
}
pub(crate) fn revtwoway(rt: &mut Runtime, s: &str) -> Result<Value> {
revoutput(rt, s)
}
pub(crate) fn rename(rt: &mut Runtime, old: &str, new: &str) -> Result<Value> {
rt.require_unsandboxed_io()?;
rt.clear_errno();
match fs::rename(old, new) {
Ok(()) => Ok(Value::Num(0.0)),
Err(e) => {
rt.set_errno_io(&e);
Ok(Value::Num(-1.0))
}
}
}
pub(crate) fn inplace_tmpfile(rt: &mut Runtime, path: &str) -> Result<Value> {
rt.require_unsandboxed_io()?;
rt.clear_errno();
let p = Path::new(path);
let dir = p
.parent()
.filter(|d| !d.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let tmp = dir.join(format!(
".{}.awkrs_inplace.{}",
p.file_name().and_then(|s| s.to_str()).unwrap_or("file"),
stamp
));
let tmp_s = tmp.to_string_lossy().into_owned();
match File::create(&tmp) {
Ok(_) => Ok(Value::Str(tmp_s)),
Err(e) => {
rt.set_errno_io(&e);
Ok(Value::Str(String::new()))
}
}
}
pub(crate) fn inplace_commit(rt: &mut Runtime, tmp: &str, dest: &str) -> Result<Value> {
rename(rt, tmp, dest)
}
fn escape_rw(s: &str) -> String {
let mut o = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\\' => o.push_str("\\\\"),
'\n' => o.push_str("\\n"),
'\r' => o.push_str("\\r"),
'\t' => o.push_str("\\t"),
_ => o.push(c),
}
}
o
}
fn unescape_rw(s: &str) -> String {
let mut o = String::with_capacity(s.len());
let mut it = s.chars();
while let Some(c) = it.next() {
if c == '\\' {
match it.next() {
Some('n') => o.push('\n'),
Some('r') => o.push('\r'),
Some('t') => o.push('\t'),
Some('\\') => o.push('\\'),
Some(x) => {
o.push('\\');
o.push(x);
}
None => o.push('\\'),
}
} else {
o.push(c);
}
}
o
}
pub(crate) fn writea(rt: &mut Runtime, path: &str, arr_name: &str) -> Result<Value> {
rt.require_unsandboxed_io()?;
rt.clear_errno();
let keys = rt.array_keys(arr_name);
let mut f = match File::create(path) {
Ok(f) => f,
Err(e) => {
rt.set_errno_io(&e);
return Ok(Value::Num(-1.0));
}
};
writeln!(f, "awkrs-rwarray-v1").map_err(Error::Io)?;
for k in keys {
let v = rt.array_get(arr_name, &k);
let line = format!("{}\t{}\n", escape_rw(&k), escape_rw(&v.as_str()));
f.write_all(line.as_bytes()).map_err(Error::Io)?;
}
Ok(Value::Num(0.0))
}
pub(crate) fn reada(rt: &mut Runtime, path: &str, arr_name: &str) -> Result<Value> {
rt.require_unsandboxed_io()?;
rt.clear_errno();
let f = match File::open(path) {
Ok(f) => f,
Err(e) => {
rt.set_errno_io(&e);
return Ok(Value::Num(-1.0));
}
};
let mut reader = BufReader::new(f);
let mut magic = String::new();
reader.read_line(&mut magic).map_err(Error::Io)?;
if magic.trim() != "awkrs-rwarray-v1" {
rt.set_errno_str("reada: not an awkrs rwarray file");
return Ok(Value::Num(-1.0));
}
rt.array_delete(arr_name, None);
let mut line = String::new();
while reader.read_line(&mut line).map_err(Error::Io)? > 0 {
let s = line.trim_end_matches(['\r', '\n']);
if s.is_empty() {
line.clear();
continue;
}
let mut parts = s.splitn(2, '\t');
let key = parts.next().unwrap_or("");
let val = parts.next().unwrap_or("");
rt.array_set(arr_name, unescape_rw(key), Value::Str(unescape_rw(val)));
line.clear();
}
Ok(Value::Num(0.0))
}
pub(crate) fn intdiv0(rt: &mut Runtime, a: &Value, b: &Value) -> Result<Value> {
match crate::bignum::awk_intdiv_values(a, b, rt) {
Ok(v) => Ok(v),
Err(_) => {
if rt.bignum {
let prec = rt.mpfr_prec_bits();
let round = rt.mpfr_round();
Ok(Value::Mpfr(rug::Float::with_val_round(prec, 0, round).0))
} else {
Ok(Value::Num(0.0))
}
}
}
}
pub(crate) fn readdir(rt: &mut Runtime, path: &str, arr_name: &str) -> Result<Value> {
rt.require_unsandboxed_io()?;
rt.clear_errno();
let entries = match fs::read_dir(path) {
Ok(rd) => rd,
Err(e) => {
rt.set_errno_io(&e);
return Ok(Value::Num(-1.0));
}
};
rt.array_delete(arr_name, None);
let mut count = 0usize;
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(e) => {
rt.set_errno_io(&e);
continue;
}
};
let fname = entry.file_name().to_string_lossy().into_owned();
let ftype = entry
.file_type()
.map(|ft| {
if ft.is_dir() {
"d"
} else if ft.is_symlink() {
"l"
} else if ft.is_file() {
"f"
} else {
"u"
}
})
.unwrap_or("u");
count += 1;
rt.array_set(
arr_name,
count.to_string(),
Value::Str(format!("{fname}/{ftype}")),
);
}
Ok(Value::Num(count as f64))
}
pub(crate) fn getlocaltime(rt: &mut Runtime, arr_name: &str, ts: Option<f64>) -> Result<Value> {
rt.clear_errno();
let epoch_secs = ts.unwrap_or_else(|| {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0)
});
let secs_i64 = epoch_secs as i64;
#[cfg(unix)]
{
use std::mem::MaybeUninit;
let mut tm: MaybeUninit<libc::tm> = MaybeUninit::uninit();
let time_t = secs_i64 as libc::time_t;
let result = unsafe { libc::localtime_r(&time_t, tm.as_mut_ptr()) };
if result.is_null() {
rt.set_errno_str("getlocaltime: localtime_r failed");
return Ok(Value::Num(-1.0));
}
let tm = unsafe { tm.assume_init() };
rt.array_delete(arr_name, None);
rt.array_set(arr_name, "sec".into(), Value::Num(tm.tm_sec as f64));
rt.array_set(arr_name, "min".into(), Value::Num(tm.tm_min as f64));
rt.array_set(arr_name, "hour".into(), Value::Num(tm.tm_hour as f64));
rt.array_set(arr_name, "mday".into(), Value::Num(tm.tm_mday as f64));
rt.array_set(arr_name, "mon".into(), Value::Num((tm.tm_mon + 1) as f64));
rt.array_set(
arr_name,
"year".into(),
Value::Num((tm.tm_year + 1900) as f64),
);
rt.array_set(arr_name, "wday".into(), Value::Num(tm.tm_wday as f64));
rt.array_set(arr_name, "yday".into(), Value::Num((tm.tm_yday + 1) as f64));
rt.array_set(arr_name, "isdst".into(), Value::Num(tm.tm_isdst as f64));
}
#[cfg(not(unix))]
{
let _ = secs_i64;
rt.array_delete(arr_name, None);
rt.set_errno_str("getlocaltime: not supported on this platform");
}
Ok(Value::Num(epoch_secs))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::Runtime;
#[test]
fn ord_chr_roundtrip() {
let mut rt = Runtime::new();
let o = ord(&mut rt, "A").unwrap();
assert_eq!(o.as_number(), 65.0);
let c = chr(&mut rt, 65.0).unwrap();
assert_eq!(c.as_str(), "A");
}
#[test]
fn intdiv0_zero_divisor() {
let mut rt = Runtime::new();
let v = intdiv0(&mut rt, &Value::Num(10.0), &Value::Num(0.0)).unwrap();
assert_eq!(v.as_number(), 0.0);
}
#[test]
fn writea_reada_roundtrip() {
let mut rt = Runtime::new();
rt.array_set("a", "x".into(), Value::Str("hello".into()));
let dir = std::env::temp_dir();
let p = dir.join("awkrs_rwarray_test.tmp");
let _ = std::fs::remove_file(&p);
writea(&mut rt, p.to_str().unwrap(), "a").unwrap();
reada(&mut rt, p.to_str().unwrap(), "b").unwrap();
assert_eq!(rt.array_get("b", "x").as_str(), "hello");
let _ = std::fs::remove_file(&p);
}
#[test]
fn ord_empty_string_zero() {
let mut rt = Runtime::new();
assert_eq!(ord(&mut rt, "").unwrap().as_number(), 0.0);
}
#[test]
fn chr_invalid_codepoint_empty_string() {
let mut rt = Runtime::new();
let v = chr(&mut rt, f64::from(0xD800)).unwrap();
assert_eq!(v.as_str(), "");
}
#[test]
fn gettimeofday_sets_sec_and_usec() {
let mut rt = Runtime::new();
gettimeofday(&mut rt, "ts").unwrap();
assert!(rt.array_get("ts", "sec").as_number() > 0.0);
let usec = rt.array_get("ts", "usec").as_number();
assert!((0.0..=999_999.0).contains(&usec), "usec={usec}");
}
#[test]
fn sleep_negative_errors() {
let mut rt = Runtime::new();
let e = sleep_secs(&mut rt, -1.0).unwrap_err();
assert!(e.to_string().contains("sleep"), "unexpected error: {e}");
}
#[test]
fn sleep_zero_returns_ok_without_panicking() {
let mut rt = Runtime::new();
sleep_secs(&mut rt, 0.0).unwrap();
}
#[test]
fn revoutput_reverses_scalar_order() {
let mut rt = Runtime::new();
let v = revoutput(&mut rt, "ab").unwrap();
assert_eq!(v.as_str(), "ba");
}
#[test]
fn readfile_missing_yields_empty_string() {
let mut rt = Runtime::new();
let dir = std::env::temp_dir();
let p = dir.join(format!("awkrs_no_such_readfile_{}", std::process::id()));
let _ = std::fs::remove_file(&p);
let v = readfile(&mut rt, p.to_str().unwrap()).unwrap();
assert_eq!(v.as_str(), "");
}
#[test]
fn reada_rejects_bad_magic() {
let mut rt = Runtime::new();
let dir = std::env::temp_dir();
let p = dir.join(format!("awkrs_reada_bad_{}", std::process::id()));
std::fs::write(&p, "not-magic\n").unwrap();
let n = reada(&mut rt, p.to_str().unwrap(), "z").unwrap();
assert_eq!(n.as_number(), -1.0);
let _ = std::fs::remove_file(&p);
}
#[test]
fn stat_populates_type_and_size_for_file() {
let mut rt = Runtime::new();
let dir = std::env::temp_dir();
let p = dir.join(format!("awkrs_stat_test_{}", std::process::id()));
std::fs::write(&p, b"hi").unwrap();
let code = stat(&mut rt, p.to_str().unwrap(), "st").unwrap();
assert_eq!(code.as_number(), 0.0);
assert_eq!(rt.array_get("st", "type").as_str(), "file");
assert_eq!(rt.array_get("st", "size").as_number(), 2.0);
let _ = std::fs::remove_file(&p);
}
#[test]
fn ord_first_char_unicode_scalar() {
let mut rt = Runtime::new();
let n = ord(&mut rt, "πx").unwrap().as_number();
assert_eq!(n, f64::from('π' as u32));
}
#[test]
fn chr_ascii_roundtrip_with_ord() {
let mut rt = Runtime::new();
let c = chr(&mut rt, 65.0).unwrap();
assert_eq!(c.as_str(), "A");
let n = ord(&mut rt, "A").unwrap().as_number();
assert_eq!(n, 65.0);
}
#[test]
fn revoutput_unicode_preserves_scalar_boundaries() {
let mut rt = Runtime::new();
let v = revoutput(&mut rt, "aπb").unwrap();
assert_eq!(v.as_str(), "bπa");
}
}