From 205f2ddd30a2ffe57e7dcf1371a3ccb1d65c7b7d Mon Sep 17 00:00:00 2001 From: syrmel <104119569+syrmel@users.noreply.github.com> Date: Wed, 7 Feb 2024 06:12:03 +0100 Subject: [PATCH] os: add os.stat() and helpers (#20739) --- vlib/os/os.v | 15 ++++++ vlib/os/os_stat_default.c.v | 81 +++++++++++++++++++++++++++++ vlib/os/os_stat_test.v | 57 ++++++++++++++++++++ vlib/os/os_stat_windows.c.v | 63 ++++++++++++++++++++++ vlib/os/os_structs_stat_default.c.v | 12 ++++- vlib/os/os_structs_stat_windows.v | 15 ++++++ 6 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 vlib/os/os_stat_default.c.v create mode 100644 vlib/os/os_stat_test.v create mode 100644 vlib/os/os_stat_windows.c.v create mode 100644 vlib/os/os_structs_stat_windows.v diff --git a/vlib/os/os.v b/vlib/os/os.v index 923269f3b5..0e9ed9bfd7 100644 --- a/vlib/os/os.v +++ b/vlib/os/os.v @@ -977,3 +977,18 @@ pub fn config_dir() !string { } return error('Cannot find config directory') } + +pub struct Stat { +pub: + dev u64 + inode u64 + mode u32 + nlink u64 + uid u32 + gid u32 + rdev u64 + size u64 + atime i64 + mtime i64 + ctime i64 +} diff --git a/vlib/os/os_stat_default.c.v b/vlib/os/os_stat_default.c.v new file mode 100644 index 0000000000..7cdac8b5f2 --- /dev/null +++ b/vlib/os/os_stat_default.c.v @@ -0,0 +1,81 @@ +module os + +// stat returns a platform-agnostic Stat struct comparable to what is +// available in other programming languages and fails with the POSIX +// error if the stat call fails. If a link is stat'd, the stat info +// for the link is provided. +pub fn stat(path string) !Stat { + mut s := C.stat{} + unsafe { + res := C.lstat(&char(path.str), &s) + if res != 0 { + return error_posix() + } + return Stat{ + dev: s.st_dev + inode: s.st_ino + nlink: s.st_nlink + mode: s.st_mode + uid: s.st_uid + gid: s.st_gid + rdev: s.st_rdev + size: s.st_size + atime: s.st_atime + mtime: s.st_mtime + ctime: s.st_ctime + } + } +} + +// get_filetype returns the FileType from the Stat struct +pub fn (st Stat) get_filetype() FileType { + match st.mode & u32(C.S_IFMT) { + u32(C.S_IFREG) { + return .regular + } + u32(C.S_IFDIR) { + return .directory + } + u32(C.S_IFCHR) { + return .character_device + } + u32(C.S_IFBLK) { + return .block_device + } + u32(C.S_IFIFO) { + return .fifo + } + u32(C.S_IFLNK) { + return .symbolic_link + } + u32(C.S_IFSOCK) { + return .socket + } + else { + return .unknown + } + } +} + +// get_mode returns the file type and permissions (readable, writable, executable) +// in owner/group/others format +pub fn (st Stat) get_mode() FileMode { + return FileMode{ + typ: st.get_filetype() + owner: FilePermission{ + read: (st.mode & u32(C.S_IRUSR)) != 0 + write: (st.mode & u32(C.S_IWUSR)) != 0 + execute: (st.mode & u32(C.S_IXUSR)) != 0 + } + group: FilePermission{ + read: (st.mode & u32(C.S_IRGRP)) != 0 + write: (st.mode & u32(C.S_IWGRP)) != 0 + execute: (st.mode & u32(C.S_IXGRP)) != 0 + } + others: FilePermission{ + read: (st.mode & u32(C.S_IROTH)) != 0 + write: (st.mode & u32(C.S_IWOTH)) != 0 + execute: (st.mode & u32(C.S_IXOTH)) != 0 + } + } +} diff --git a/vlib/os/os_stat_test.v b/vlib/os/os_stat_test.v new file mode 100644 index 0000000000..37d42bf410 --- /dev/null +++ b/vlib/os/os_stat_test.v @@ -0,0 +1,57 @@ +import os +import rand +import time + +fn test_stat() { + start_time := time.utc() + + temp_dir := os.join_path(os.temp_dir(), rand.ulid()) + os.mkdir(temp_dir)! + defer { + os.rmdir(temp_dir) or {} + } + + test_file := os.join_path(temp_dir, rand.ulid()) + test_content := rand.ulid() + os.write_file(test_file, test_content)! + defer { + os.rm(test_file) or {} + } + + end_time := time.utc() + + mut fstat := os.stat(test_file)! + assert fstat.get_filetype() == .regular + assert fstat.size == u64(test_content.len) + assert fstat.ctime >= start_time.unix + assert fstat.ctime <= end_time.unix + assert fstat.mtime >= start_time.unix + assert fstat.mtime <= end_time.unix + + $if !windows { + os.chmod(test_file, 0o600)! + fstat = os.stat(test_file)! + + mut fmode := fstat.get_mode() + assert fmode.typ == .regular + assert fmode.owner.read && fmode.owner.write && !fmode.owner.execute + assert !fmode.group.read && !fmode.group.write && !fmode.group.execute + assert !fmode.others.read && !fmode.others.write && !fmode.others.execute + + os.chmod(test_file, 0o421)! + fstat = os.stat(test_file)! + fmode = fstat.get_mode() + assert fmode.owner.read && !fmode.owner.write && !fmode.owner.execute + assert !fmode.group.read && fmode.group.write && !fmode.group.execute + assert !fmode.others.read && !fmode.others.write && fmode.others.execute + + os.chmod(test_file, 0o600)! + } + + // When using the Time struct, allow for up to 1 second difference due to nanoseconds + // which are not captured in the timestamp + dstat := os.stat(temp_dir)! + assert dstat.get_filetype() == .directory + assert fstat.dev == dstat.dev, 'File and directory should be created on same device' + assert fstat.rdev == dstat.rdev, 'File and directory should have same device ID' +} diff --git a/vlib/os/os_stat_windows.c.v b/vlib/os/os_stat_windows.c.v new file mode 100644 index 0000000000..2e3b3bc762 --- /dev/null +++ b/vlib/os/os_stat_windows.c.v @@ -0,0 +1,63 @@ +module os + +// stat returns a platform-agnostic Stat struct comparable to what is +// available in other programming languages and fails with the POSIX +// error if the stat call fails. If a link is stat'd, the stat info +// for the link is provided. +pub fn stat(path string) !Stat { + mut s := C.__stat64{} + unsafe { + res := C._wstat64(path.to_wide(), &s) + if res != 0 { + return error_posix() + } + return Stat{ + dev: s.st_dev + inode: s.st_ino + nlink: s.st_nlink + mode: s.st_mode + uid: s.st_uid + gid: s.st_gid + rdev: s.st_rdev + size: s.st_size + atime: s.st_atime + mtime: s.st_mtime + ctime: s.st_ctime + } + } +} + +// get_filetype returns the FileType from the Stat struct +pub fn (st Stat) get_filetype() FileType { + match st.mode & u32(C.S_IFMT) { + u32(C.S_IFDIR) { + return .directory + } + else { + return .regular + } + } +} + +// get_mode returns the file type and permissions (readable, writable, executable) +// in owner/group/others format, however, they will all be the same for Windows +pub fn (st Stat) get_mode() FileMode { + return FileMode{ + typ: st.get_filetype() + owner: FilePermission{ + read: (st.mode & u32(C.S_IREAD)) != 0 + write: (st.mode & u32(C.S_IWRITE)) != 0 + execute: (st.mode & u32(C.S_IEXEC)) != 0 + } + group: FilePermission{ + read: (st.mode & u32(C.S_IREAD)) != 0 + write: (st.mode & u32(C.S_IWRITE)) != 0 + execute: (st.mode & u32(C.S_IEXEC)) != 0 + } + others: FilePermission{ + read: (st.mode & u32(C.S_IREAD)) != 0 + write: (st.mode & u32(C.S_IWRITE)) != 0 + execute: (st.mode & u32(C.S_IEXEC)) != 0 + } + } +} diff --git a/vlib/os/os_structs_stat_default.c.v b/vlib/os/os_structs_stat_default.c.v index de50734631..45aca00764 100644 --- a/vlib/os/os_structs_stat_default.c.v +++ b/vlib/os/os_structs_stat_default.c.v @@ -1,9 +1,19 @@ module os +// Minimal stat struct as specified in +// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/sys_stat.h.html pub struct C.stat { - st_size u64 + st_dev u64 + st_ino u64 st_mode u32 + st_nlink u64 + st_uid u32 + st_gid u32 + st_rdev u64 + st_size u64 + st_atime int st_mtime int + st_ctime int } pub struct C.__stat64 { diff --git a/vlib/os/os_structs_stat_windows.v b/vlib/os/os_structs_stat_windows.v new file mode 100644 index 0000000000..c411a0a0ed --- /dev/null +++ b/vlib/os/os_structs_stat_windows.v @@ -0,0 +1,15 @@ +module os + +pub struct C.__stat64 { + st_dev u32 // 4 + st_ino u16 // 2 + st_mode u16 // 2 + st_nlink u16 // 2 + st_uid u16 // 2 + st_gid u16 // 2 + st_rdev u32 // 4 + st_size u64 // 8 + st_atime i64 // 8 + st_mtime i64 // 8 + st_ctime i64 // 8 +}