Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions apps/nest-node-probe/correctness/fs-stat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { existsSync, promises as fsPromises, stat, statSync, Stats } from 'node:fs'

interface StatsReport {
dev: number
ino: number
mode: number
nlink: number
uid: number
gid: number
rdev: number
size: number
blksize: number
blocks: number
atimePlausible: boolean
mtimeMs: number
ctimeMs: number
birthtimeMs: number
mtime: string
ctime: string
birthtime: string
isFile: boolean
isDirectory: boolean
isBlockDevice: boolean
isCharacterDevice: boolean
isSymbolicLink: boolean
isFIFO: boolean
isSocket: boolean
}

interface ErrorReport {
message: string
code: string
errno: number
syscall: string
path: string
}

function report(stats: Stats, volatileTimes = false): StatsReport {
const timeMs = (ms: number): number => (volatileTimes ? -1 : ms)
const time = (date: Date): string => (volatileTimes ? 'volatile' : date.toISOString())
return {
dev: stats.dev,
ino: stats.ino,
mode: stats.mode,
nlink: stats.nlink,
uid: stats.uid,
gid: stats.gid,
rdev: stats.rdev,
size: stats.size,
blksize: stats.blksize,
blocks: stats.blocks,
atimePlausible: stats.atimeMs > 0 && stats.atime.getTime() === Math.round(stats.atimeMs),
mtimeMs: timeMs(stats.mtimeMs),
ctimeMs: timeMs(stats.ctimeMs),
birthtimeMs: timeMs(stats.birthtimeMs),
mtime: time(stats.mtime),
ctime: time(stats.ctime),
birthtime: time(stats.birthtime),
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
isBlockDevice: stats.isBlockDevice(),
isCharacterDevice: stats.isCharacterDevice(),
isSymbolicLink: stats.isSymbolicLink(),
isFIFO: stats.isFIFO(),
isSocket: stats.isSocket()
}
}

function errorReport(error: unknown): ErrorReport {
if (error instanceof Error && 'code' in error && 'errno' in error && 'syscall' in error && 'path' in error) {
return {
message: error.message,
code: String(error.code),
errno: Number(error.errno),
syscall: String(error.syscall),
path: String(error.path)
}
}
return { message: String(error), code: '', errno: 0, syscall: '', path: '' }
}

const file = 'apps/nest-node-probe/correctness/fs-stat.ts'
const directory = 'runtime/node'
const device = '/dev/null'
const missing = 'apps/nest-node-probe/correctness/does-not-exist'
const notDirectory = 'apps/nest-node-probe/correctness/fs-stat.ts/child'

async function main(): Promise<void> {
console.log(JSON.stringify(report(statSync(file))))
console.log(JSON.stringify(report(statSync(directory))))
console.log(JSON.stringify(report(statSync(device), true)))
console.log(JSON.stringify(report(statSync(new URL(`file://${process.cwd()}/${file}`)))))
console.log(existsSync(file), existsSync(missing))

for (const path of [missing, notDirectory]) {
try {
statSync(path)
console.log('no error')
} catch (error) {
console.log(JSON.stringify(errorReport(error)))
}
}

console.log(statSync(missing, { throwIfNoEntry: false }) === undefined, statSync(notDirectory, { throwIfNoEntry: false }) === undefined)
const present = statSync(file, { throwIfNoEntry: false })
console.log(present === undefined ? 'undefined' : JSON.stringify(report(present)))
try {
statSync(missing, { throwIfNoEntry: true })
console.log('no error')
} catch (error) {
console.log(JSON.stringify(errorReport(error)))
}

const fromPromise = await fsPromises.stat(directory)
console.log(JSON.stringify(report(fromPromise)))
try {
await fsPromises.stat(missing)
console.log('no error')
} catch (error) {
console.log(JSON.stringify(errorReport(error)))
}

await new Promise<void>((resolve) => {
let ordered = 'callback ran before stat returned'
stat(file, (err, stats) => {
console.log(ordered)
console.log(err === null, stats === undefined ? 'undefined' : JSON.stringify(report(stats)))
stat(missing, (missingErr, missingStats) => {
console.log(missingErr === null ? 'null' : JSON.stringify(errorReport(missingErr)), missingStats === undefined)
resolve()
})
})
ordered = 'callback ran after stat returned'
})
}

main()
10 changes: 10 additions & 0 deletions plugin/intrinsics.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,16 @@ export const intrinsics = {
returnType: 'bool',
decl: 'bool __gea_node_fs_access(std::string path, double mode);'
},
__gea_node_fs_stat: {
emit: '__gea_node_fs_stat',
returnType: 'double',
decl: 'double __gea_node_fs_stat(std::string path, bool followSymlinks);'
},
__gea_node_fs_stat_field: {
emit: '__gea_node_fs_stat_field',
returnType: 'double',
decl: 'double __gea_node_fs_stat_field(double field);'
},
// These return the native Buffer carrier directly. Their declarations live
// in gea_node_buffer.hpp, which generated_support.hpp includes, so no
// gea_cpp_value bridge is introduced for random/file bytes.
Expand Down
32 changes: 32 additions & 0 deletions runtime/gea_node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1881,15 +1881,20 @@ inline std::string net_errno_name(double requested) {
GEA_NODE_ERRNO_NAME(EINVAL)
GEA_NODE_ERRNO_NAME(EIO)
GEA_NODE_ERRNO_NAME(EISCONN)
GEA_NODE_ERRNO_NAME(ELOOP)
GEA_NODE_ERRNO_NAME(EMFILE)
GEA_NODE_ERRNO_NAME(EMSGSIZE)
GEA_NODE_ERRNO_NAME(ENAMETOOLONG)
GEA_NODE_ERRNO_NAME(ENETDOWN)
GEA_NODE_ERRNO_NAME(ENETUNREACH)
GEA_NODE_ERRNO_NAME(ENFILE)
GEA_NODE_ERRNO_NAME(ENOBUFS)
GEA_NODE_ERRNO_NAME(ENOENT)
GEA_NODE_ERRNO_NAME(ENOMEM)
GEA_NODE_ERRNO_NAME(ENOTCONN)
GEA_NODE_ERRNO_NAME(ENOTDIR)
GEA_NODE_ERRNO_NAME(ENOTSOCK)
GEA_NODE_ERRNO_NAME(EOVERFLOW)
GEA_NODE_ERRNO_NAME(EPERM)
GEA_NODE_ERRNO_NAME(EPIPE)
GEA_NODE_ERRNO_NAME(EPROTO)
Expand Down Expand Up @@ -4101,6 +4106,33 @@ inline bool __gea_node_fs_access(std::string path, double mode) {
return gea::node::platform::canAccess(path, static_cast<int>(mode));
}

static gea::node::platform::FileStatus gea_node_fs_last_stat;

inline double __gea_node_fs_stat(std::string path, bool followSymlinks) {
return static_cast<double>(gea::node::platform::statFile(path, followSymlinks, gea_node_fs_last_stat));
}

inline double __gea_node_fs_stat_field(double field) {
const gea::node::platform::FileStatus &st = gea_node_fs_last_stat;
switch (static_cast<int>(field)) {
case 0: return st.dev;
case 1: return st.ino;
case 2: return st.mode;
case 3: return st.nlink;
case 4: return st.uid;
case 5: return st.gid;
case 6: return st.rdev;
case 7: return st.size;
case 8: return st.blksize;
case 9: return st.blocks;
case 10: return st.atimeMs;
case 11: return st.mtimeMs;
case 12: return st.ctimeMs;
case 13: return st.birthtimeMs;
default: return 0.0;
}
}

// The same refusal under both runtimes, raised the way each one raises: v1
// writes its global thrown slot and sets the flag, v2 throws a real C++
// exception carrying the identical `gea::Value` payload -- which is also what
Expand Down
21 changes: 20 additions & 1 deletion runtime/gea_node_platform.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
// signal handler) writes to wake the loop
// startThread(entry, argument) a detached thread; false when none starts
// utcTime(seconds, tm) gmtime
// canAccess / readWholeFile / fillRandom
// canAccess / readWholeFile / statFile / fillRandom
// arguments / environmentEntries / executablePath / canonicalPath /
// currentDirectory / processId / parentProcessId / exitImmediately /
// onlineProcessors / machineArch / kernelRelease
Expand All @@ -52,6 +52,25 @@
// Everything here is inline: the reactor is compiled into the program's own
// translation unit (see `gea_node.hpp`), and so is this.

namespace gea::node::platform {
struct FileStatus {
double dev = 0;
double ino = 0;
double mode = 0;
double nlink = 0;
double uid = 0;
double gid = 0;
double rdev = 0;
double size = 0;
double blksize = 0;
double blocks = 0;
double atimeMs = 0;
double mtimeMs = 0;
double ctimeMs = 0;
double birthtimeMs = 0;
};
} // namespace gea::node::platform

#if defined(_WIN32)
#include "gea_node_platform_win32.hpp"
#else
Expand Down
40 changes: 40 additions & 0 deletions runtime/gea_node_platform_posix.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,46 @@ inline void utcTime(std::time_t seconds, std::tm &out) { ::gmtime_r(&seconds, &o

inline bool canAccess(const std::string &path, int mode) { return ::access(path.c_str(), mode) == 0; }

inline double fileTimeMs(const struct timespec &time) {
return static_cast<double>(time.tv_sec) * 1000.0 + static_cast<double>(time.tv_nsec) / 1000000.0;
}

inline int statFile(const std::string &path, bool followSymlinks, FileStatus &out) {
struct stat st;
const int rc = followSymlinks ? ::stat(path.c_str(), &st) : ::lstat(path.c_str(), &st);
if (rc != 0) return uvErrorNumber(errno);
const auto widen = [](auto value) { return static_cast<double>(static_cast<std::uint64_t>(value)); };
out.dev = widen(st.st_dev);
out.ino = widen(st.st_ino);
out.mode = widen(st.st_mode);
out.nlink = widen(st.st_nlink);
out.uid = widen(st.st_uid);
out.gid = widen(st.st_gid);
out.rdev = widen(st.st_rdev);
out.size = widen(st.st_size);
out.blksize = widen(st.st_blksize);
out.blocks = widen(st.st_blocks);
#if defined(__APPLE__)
out.atimeMs = fileTimeMs(st.st_atimespec);
out.mtimeMs = fileTimeMs(st.st_mtimespec);
out.ctimeMs = fileTimeMs(st.st_ctimespec);
out.birthtimeMs = fileTimeMs(st.st_birthtimespec);
#else
out.atimeMs = fileTimeMs(st.st_atim);
out.mtimeMs = fileTimeMs(st.st_mtim);
out.ctimeMs = fileTimeMs(st.st_ctim);
out.birthtimeMs = out.ctimeMs;
#if defined(__linux__) && defined(STATX_BTIME)
struct statx stx;
const int flags = followSymlinks ? 0 : AT_SYMLINK_NOFOLLOW;
if (::statx(AT_FDCWD, path.c_str(), flags, STATX_BTIME, &stx) == 0 && (stx.stx_mask & STATX_BTIME) != 0) {
out.birthtimeMs = static_cast<double>(stx.stx_btime.tv_sec) * 1000.0 + static_cast<double>(stx.stx_btime.tv_nsec) / 1000000.0;
}
#endif
#endif
return 0;
}

inline bool readWholeFile(const std::string &path, std::vector<std::uint8_t> &out, std::string &failure) {
const int fd = ::open(path.c_str(), O_RDONLY);
if (fd < 0) {
Expand Down
45 changes: 45 additions & 0 deletions runtime/gea_node_platform_win32.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,51 @@ inline bool canAccess(const std::string &path, int mode) {
return true;
}

inline double fileTimeMs(const LARGE_INTEGER &time) {
return (static_cast<double>(time.QuadPart) - 116444736000000000.0) / 10000.0;
}

// The mode bits libuv reports on Windows: a type from the attributes and the
// handle, read for everyone, written unless the read-only attribute is set.
inline int statFile(const std::string &path, bool followSymlinks, FileStatus &out) {
const DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | (followSymlinks ? 0 : FILE_FLAG_OPEN_REPARSE_POINT);
const HANDLE file = ::CreateFileW(wide(path).c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_EXISTING, flags, nullptr);
if (file == INVALID_HANDLE_VALUE) return uvErrorNumber(errnoFromWin32(::GetLastError()));
BY_HANDLE_FILE_INFORMATION info;
FILE_BASIC_INFO basic;
FILE_STANDARD_INFO standard;
if (!::GetFileInformationByHandle(file, &info) || !::GetFileInformationByHandleEx(file, FileBasicInfo, &basic, sizeof(basic)) ||
!::GetFileInformationByHandleEx(file, FileStandardInfo, &standard, sizeof(standard))) {
const DWORD error = ::GetLastError();
::CloseHandle(file);
return uvErrorNumber(errnoFromWin32(error));
}
FILE_STORAGE_INFO storage;
const bool hasStorage = ::GetFileInformationByHandleEx(file, FileStorageInfo, &storage, sizeof(storage)) != 0;
const DWORD type = ::GetFileType(file);
::CloseHandle(file);
const bool directory = (info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
const bool link = !followSymlinks && (info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
const unsigned typeBits = link ? 0120000u : directory ? 0040000u : type == FILE_TYPE_CHAR ? 0020000u : 0100000u;
const unsigned permissionBits = (info.dwFileAttributes & FILE_ATTRIBUTE_READONLY) != 0 ? 0444u : 0666u;
out.dev = static_cast<double>(info.dwVolumeSerialNumber);
out.ino = static_cast<double>((static_cast<std::uint64_t>(info.nFileIndexHigh) << 32) | info.nFileIndexLow);
out.mode = static_cast<double>(typeBits | permissionBits);
out.nlink = static_cast<double>(info.nNumberOfLinks);
out.uid = 0;
out.gid = 0;
out.rdev = 0;
out.size = directory ? 0.0 : static_cast<double>(standard.EndOfFile.QuadPart);
out.blksize = hasStorage ? static_cast<double>(storage.PhysicalBytesPerSectorForPerformance) : 4096.0;
out.blocks = static_cast<double>((standard.AllocationSize.QuadPart + 511) / 512);
out.atimeMs = fileTimeMs(basic.LastAccessTime);
out.mtimeMs = fileTimeMs(basic.LastWriteTime);
out.ctimeMs = fileTimeMs(basic.ChangeTime);
out.birthtimeMs = fileTimeMs(basic.CreationTime);
return 0;
}

inline bool readWholeFile(const std::string &path, std::vector<std::uint8_t> &out, std::string &failure) {
const std::wstring name = wide(path);
const DWORD attributes = ::GetFileAttributesW(name.c_str());
Expand Down
Loading
Loading