use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::sync::atomic::{AtomicU32, Ordering};
use rt::os::*;
use super::{env, time_us};
use crate::{FO_READ, FO_RW, FO_WRITE};
const AT_REMOVEDIR: usize = 0x200;
const EPERM: isize = 1;
const EINTR: isize = 4;
const EEXIST: isize = 17;
const EISDIR: isize = 21;
const S_IFMT: u32 = 0xF000;
const S_IFDIR: u32 = 0x4000;
const S_IFREG: u32 = 0x8000;
const LINK_MAX: usize = 40; const IO_CHUNK: usize = 0x4000_0000;
const TEMP_TRIES: u32 = 16;
const DL_BUF: usize = 32768;
fn path_z(path: &str) -> Vec<u8> {
let mut p: Vec<u8> = path.as_bytes().to_vec();
p.push(0);
p
}
fn stat_at(dirfd: usize, name: *const u8) -> Option<(u32, i64)> {
let mut st = Stat::default();
if unsafe { syscall6(SYS_NEWFSTATAT, dirfd, name as usize, &mut st as *mut Stat as usize, 0, 0, 0) } != 0 {
return None;
}
Some((st.st_mode, st.st_size))
}
pub fn dev_ino(path: &str) -> Option<(u64, u64)> {
let p = path_z(path);
let mut st = Stat::default();
if unsafe { syscall6(SYS_NEWFSTATAT, AT_FDCWD, p.as_ptr() as usize, &mut st as *mut Stat as usize, 0, 0, 0) } != 0 {
return None;
}
Some((st.st_dev, st.st_ino))
}
fn trim_slash(mut s: String) -> String {
while s.len() > 1 && s.ends_with('/') {
s.pop();
}
s
}
pub fn config_dir() -> Option<String> {
if let Some(v) = env("XDG_CONFIG_HOME").filter(|v| v.starts_with('/')) {
return Some(trim_slash(v));
}
let home = trim_slash(env("HOME").filter(|v| !v.is_empty())?);
let s = if home.ends_with('/') { home + ".config" } else { home + "/.config" };
Some(trim_slash(s))
}
pub fn data_dir() -> Option<String> {
xdg_dir("XDG_DATA_HOME", ".local/share")
}
pub fn cache_dir() -> Option<String> {
xdg_dir("XDG_CACHE_HOME", ".cache")
}
fn xdg_dir(var: &str, fallback: &str) -> Option<String> {
if let Some(v) = env(var).filter(|v| v.starts_with('/')) {
return Some(trim_slash(v));
}
let home = trim_slash(env("HOME").filter(|v| !v.is_empty())?);
let s = if home.ends_with('/') { home + fallback } else { home + "/" + fallback };
Some(trim_slash(s))
}
pub fn temp_dir() -> Option<String> {
match env("TMPDIR").filter(|v| v.starts_with('/')) {
Some(v) => Some(trim_slash(v)),
None => Some(String::from("/tmp")),
}
}
pub fn cwd() -> String {
let mut buf = alloc::vec![0u8; 4096];
let r = unsafe { syscall3(SYS_GETCWD, buf.as_mut_ptr() as usize, buf.len(), 0) };
if r <= 0 {
return String::new();
}
let n = buf.iter().position(|&c| c == 0).unwrap_or(0);
String::from_utf8_lossy(&buf[..n]).into_owned()
}
pub(crate) fn file_exists_raw(path: &str) -> u32 {
let p = path_z(path);
match stat_at(AT_FDCWD, p.as_ptr()) {
None => 0,
Some((m, _)) if m & S_IFMT == S_IFDIR => 2,
Some(_) => 1,
}
}
fn mkdir1(path: &[u8]) -> bool {
let mut p = path.to_vec();
p.push(0);
let r = unsafe { syscall3(SYS_MKDIRAT, AT_FDCWD, p.as_ptr() as usize, 0o777) };
if r == 0 {
return true;
}
r == -EEXIST && matches!(stat_at(AT_FDCWD, p.as_ptr()), Some((m, _)) if m & S_IFMT == S_IFDIR)
}
pub(crate) fn mkdir_raw(path: &str) -> bool {
let b = path.as_bytes();
if mkdir1(b) {
return true;
}
for i in 1..b.len() {
if b[i] == b'/' && b[i - 1] != b'/' {
mkdir1(&b[..i]); }
}
mkdir1(b)
}
pub(crate) fn delete_raw(path: &str) -> bool {
let p = path_z(path);
unsafe {
let r = syscall3(SYS_UNLINKAT, AT_FDCWD, p.as_ptr() as usize, 0);
if r == 0 {
return true;
}
(r == -EISDIR || r == -EPERM) && syscall3(SYS_UNLINKAT, AT_FDCWD, p.as_ptr() as usize, AT_REMOVEDIR) == 0
}
}
pub(crate) fn rename_raw(old: &str, new: &str) -> bool {
let (o, n) = (path_z(old), path_z(new));
unsafe { rt::os::rename(o.as_ptr() as usize, n.as_ptr() as usize) == 0 }
}
pub(crate) fn dir_list_raw(path: &str, f: &mut dyn FnMut(&str, bool, u32)) -> bool {
let p = path_z(path);
let fd = unsafe { syscall6(SYS_OPENAT, AT_FDCWD, p.as_ptr() as usize, O_DIRECTORY | O_CLOEXEC, 0, 0, 0) };
if fd < 0 {
return false;
}
let fd = fd as usize;
let mut buf = alloc::vec![0u8; DL_BUF];
let mut ok = true;
'fill: loop {
let n = unsafe { syscall3(SYS_GETDENTS64, fd, buf.as_mut_ptr() as usize, DL_BUF) };
if n == -EINTR {
continue;
}
if n < 0 {
ok = false;
break;
}
if n == 0 {
break;
}
let mut off = 0usize;
while off < n as usize {
let reclen = u16::from_le_bytes([buf[off + 16], buf[off + 17]]) as usize;
if reclen == 0 {
break 'fill;
}
let dtype = buf[off + 18];
let name = &buf[off + 19..off + reclen];
let name = &name[..name.iter().position(|&c| c == 0).unwrap_or(name.len())];
if name != b"." && name != b".." {
let mut nz = name.to_vec();
nz.push(0);
let (is_dir, size) = match stat_at(fd, nz.as_ptr()) {
Some((m, s)) => (m & S_IFMT == S_IFDIR, s as u32),
None => (dtype == 4, 0), };
f(&String::from_utf8_lossy(name), is_dir, size);
}
off += reclen;
}
}
unsafe { syscall3(SYS_CLOSE, fd, 0, 0) };
ok
}
pub(crate) fn read_file_raw(path: &str) -> Option<Vec<u8>> {
let f = OsFile::open_raw(path, FO_READ)?;
let size = f.size()?;
if size > 0x7FFF_FFF0 {
return None;
}
let mut probe: Vec<u8> = Vec::new();
probe.try_reserve_exact(size as usize + 4096).ok()?;
drop(probe);
let mut data = alloc::vec![0u8; size as usize + 4096];
let mut n = 0;
loop {
n += f.read_at(n as u64, &mut data[n..])?;
if n < data.len() {
break; }
data.try_reserve_exact(data.len()).ok()?;
data.resize(data.len() * 2, 0);
}
data.truncate(n);
Some(data)
}
pub(crate) fn write_file_raw(path: &str, data: &[u8]) -> bool {
OsAtomic::begin(path).is_some_and(|a| a.write_at(0, data) == Some(data.len()) && a.commit())
}
pub(crate) struct OsAtomic {
f: Option<OsFile>,
tmp: Vec<u8>, dst: Vec<u8>,
mode: Option<usize>,
owner: (usize, usize), }
fn link_target(path: &str) -> Option<String> {
let mut cur = String::from(path);
for _ in 0..LINK_MAX {
let z = path_z(&cur);
let mut buf = [0u8; 4096];
let n = unsafe { syscall6(SYS_READLINKAT, AT_FDCWD, z.as_ptr() as usize, buf.as_mut_ptr() as usize, buf.len(), 0, 0) };
if n == -22 || n == -2 { return Some(cur); } if n <= 0 || n as usize >= buf.len() { return None; }
let t = core::str::from_utf8(&buf[..n as usize]).ok()?;
cur = if t.starts_with('/') {
String::from(t)
} else {
match cur.rfind('/') {
Some(k) => format!("{}{}", &cur[..=k], t),
None => String::from(t),
}
};
}
None
}
impl OsAtomic {
pub(crate) fn begin(path: &str) -> Option<OsAtomic> {
if path.is_empty() || path.contains('\0') { return None; }
let path = link_target(path)?;
let path = path.as_str();
let dst = path_z(path);
let mut st = Stat::default();
let status = unsafe { syscall6(SYS_NEWFSTATAT, AT_FDCWD, dst.as_ptr() as usize, &mut st as *mut Stat as usize, 0, 0, 0) };
let mode = match status {
0 if st.st_mode & S_IFMT == S_IFREG => Some(st.st_mode as usize & 0o777),
-2 => None, _ => return None,
};
let pid = unsafe { syscall3(SYS_GETPID, 0, 0, 0) } as u32;
for _ in 0..TEMP_TRIES {
let seq = TEMP_SEQ.fetch_add(1, Ordering::Relaxed);
let tmp = path_z(&format!("{path}.tmp-{pid:x}-{seq:x}-{:x}", time_us() as u32));
let fd = unsafe { syscall6(SYS_OPENAT, AT_FDCWD, tmp.as_ptr() as usize,
O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, if mode.is_some() { 0o600 } else { 0o666 }, 0, 0) };
if fd < 0 {
if fd == -EEXIST { continue; }
return None;
}
let owner = (st.st_uid as usize, st.st_gid as usize);
return Some(OsAtomic { f: Some(OsFile(fd as i32)), tmp, dst, mode, owner });
}
None
}
pub(crate) fn write_at(&self, off: u64, data: &[u8]) -> Option<usize> {
self.f.as_ref()?.write_at(off, data)
}
pub(crate) fn commit(mut self) -> bool {
let Some(f) = self.f.take() else { return false };
let mut ok = true;
if let Some(mode) = self.mode {
unsafe { syscall3(SYS_FCHOWN, f.0 as usize, self.owner.0, self.owner.1) };
ok &= unsafe { syscall3(SYS_FCHMOD, f.0 as usize, mode, 0) } == 0;
}
ok &= f.sync();
drop(f);
if ok && unsafe { rt::os::rename(self.tmp.as_ptr() as usize, self.dst.as_ptr() as usize) } == 0 {
self.tmp.clear();
return true;
}
false
}
}
impl Drop for OsAtomic {
fn drop(&mut self) {
drop(self.f.take());
if !self.tmp.is_empty() {
unsafe { syscall3(SYS_UNLINKAT, AT_FDCWD, self.tmp.as_ptr() as usize, 0) };
}
}
}
pub(crate) struct OsFile(i32);
pub(crate) struct OsFileLock { _file: OsFile }
impl OsFileLock {
pub(crate) fn acquire_raw(path: &str) -> Option<Self> {
if !matches!(file_exists_checked_raw(path), Some(0 | 1)) { return None; }
let file = OsFile::open_raw(path, FO_RW)?;
let result = unsafe { syscall3(SYS_FLOCK, file.0 as usize, 6, 0) };
if result == 0 { Some(Self { _file: file }) } else { None }
}
}
pub(crate) fn file_exists_checked_raw(path: &str) -> Option<u32> {
if path.is_empty() || path.contains('\0') { return None; }
let p = path_z(path);
let mut st = Stat::default();
let result = unsafe { syscall6(SYS_NEWFSTATAT, AT_FDCWD, p.as_ptr() as usize, &mut st as *mut Stat as usize, 0, 0, 0) };
match result {
0 => match st.st_mode & S_IFMT { S_IFDIR => Some(2), S_IFREG => Some(1), _ => None },
-2 | -20 => Some(0), _ => None,
}
}
static TEMP_SEQ: AtomicU32 = AtomicU32::new(0);
impl OsFile {
pub(crate) fn create_new_raw(path: &str) -> Option<OsFile> {
let p = path_z(path);
let fd = unsafe { syscall6(SYS_OPENAT, AT_FDCWD, p.as_ptr() as usize,
O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0o600, 0, 0) };
if fd < 0 { None } else { Some(OsFile(fd as i32)) }
}
pub(crate) fn open_raw(path: &str, mode: u32) -> Option<OsFile> {
let flags = match mode {
FO_READ => O_CLOEXEC,
FO_WRITE => O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC,
FO_RW => O_RDWR | O_CREAT | O_CLOEXEC,
_ => return None,
};
let p = path_z(path);
let fd = unsafe { syscall6(SYS_OPENAT, AT_FDCWD, p.as_ptr() as usize, flags, 0o666, 0, 0) };
if fd < 0 { None } else { Some(OsFile(fd as i32)) }
}
pub(crate) fn open_read_nofollow(path: &str) -> Option<OsFile> {
let p = path_z(path);
let fd = unsafe { syscall6(SYS_OPENAT, AT_FDCWD, p.as_ptr() as usize, O_CLOEXEC | O_NOFOLLOW, 0, 0, 0) };
if fd < 0 { None } else { Some(OsFile(fd as i32)) }
}
pub(crate) fn open_temp_raw(dir: Option<&str>) -> Option<OsFile> {
let dir = match dir {
Some(d) => String::from(d),
None => temp_dir()?,
};
let dz = path_z(&dir);
let fd = unsafe { syscall6(SYS_OPENAT, AT_FDCWD, dz.as_ptr() as usize, O_TMPFILE | O_RDWR | O_CLOEXEC, 0o600, 0, 0) };
if fd >= 0 {
return Some(OsFile(fd as i32));
}
let pid = unsafe { syscall3(SYS_GETPID, 0, 0, 0) } as u32;
for _ in 0..TEMP_TRIES {
let seq = TEMP_SEQ.fetch_add(1, Ordering::SeqCst);
let name = path_z(&format!("{dir}/.jfgui-{pid:x}-{seq:x}-{:x}.tmp", time_us() as u32));
let fd = unsafe {
syscall6(SYS_OPENAT, AT_FDCWD, name.as_ptr() as usize, O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0o600, 0, 0)
};
if fd >= 0 {
unsafe { syscall3(SYS_UNLINKAT, AT_FDCWD, name.as_ptr() as usize, 0) };
return Some(OsFile(fd as i32));
}
if fd != -EEXIST {
return None;
}
}
None
}
pub fn read_at(&self, off: u64, buf: &mut [u8]) -> Option<usize> {
let mut done = 0usize;
while done < buf.len() {
let n = (buf.len() - done).min(IO_CHUNK);
let r = unsafe {
syscall6(SYS_PREAD64, self.0 as usize, buf.as_mut_ptr() as usize + done, n, off as usize + done, 0, 0)
};
if r == -EINTR {
continue;
}
if r < 0 {
if done > 0 {
break;
}
return None;
}
if r == 0 {
break; }
done += r as usize;
}
Some(done)
}
pub fn write_at(&self, off: u64, buf: &[u8]) -> Option<usize> {
let mut done = 0usize;
while done < buf.len() {
let n = (buf.len() - done).min(IO_CHUNK);
let r = unsafe {
syscall6(SYS_PWRITE64, self.0 as usize, buf.as_ptr() as usize + done, n, off as usize + done, 0, 0)
};
if r == -EINTR {
continue;
}
if r < 0 {
if done > 0 {
break;
}
return None;
}
if r == 0 {
break;
}
done += r as usize;
}
Some(done)
}
pub fn size(&self) -> Option<u64> {
let mut st = Stat::default();
if unsafe { syscall3(SYS_FSTAT, self.0 as usize, &mut st as *mut Stat as usize, 0) } != 0 {
return None;
}
Some(st.st_size as u64)
}
pub fn sync(&self) -> bool {
unsafe { syscall3(SYS_FSYNC, self.0 as usize, 0, 0) == 0 }
}
pub fn set_len(&self, len: u64) -> bool {
unsafe { syscall3(SYS_FTRUNCATE, self.0 as usize, len as usize, 0) == 0 }
}
}
impl Drop for OsFile {
fn drop(&mut self) {
unsafe { syscall3(SYS_CLOSE, self.0 as usize, 0, 0) };
}
}