Files

151 lines
5.7 KiB
Plaintext

import "@ffi/c"
import "@std/mem"
import "@std/io"
import "@std/arraylist"
PathError :: enum { invalid_path, path_failed, directory_failed }
Error :: alias mem.AllocError | PathError
ReadFileError :: alias Error | io.OpenError | io.CloseError | io.ReadError | io.ReadLimitError
Kind :: enum { missing, file, directory, link, unknown, other }
#! owns a native directory handle; release it exactly once with close_directory.
Directory :: struct { handle ?*mut c.DIR }
#! name borrows directory-owned storage until the next call to next or close_directory.
#! enumeration may report link or unknown; kind follows links when classification is needed.
Entry :: struct {
name []u8
kind Kind
}
#! returns caller-owned zero-terminated storage; release it with mem.free using the same allocator.
@hide:file
terminated proc(allocator mem.Allocator, text []u8) []mut u8 ! Error {
if (text.len == 0 or text.len == maxval!(usize)) return .invalid_path
for (text) |byte| {
if (byte == 0) return .invalid_path
}
bytes :: try mem.alloc(u8, allocator, text.len + 1)
memcopy!(bytes[..text.len], text)
bytes[text.len] = 0
return bytes
}
#! returns an owned absolute path with symlinks resolved; release it with mem.free using the same allocator.
#! temporary path storage uses `allocator` and is released before returning.
canonical proc(allocator mem.Allocator, path []u8) []mut u8 ! Error {
input :: try terminated(allocator, path)
defer mem.free(allocator, input)
# Darwin realpath requires a PATH_MAX-sized caller buffer, including its terminator.
buffer [1024]mut u8 := undefined
if (c.realpath(ptrcast!(input.ptr), ptrcast!(buffer[..].ptr)) == null) {
if (c.__error()?^ == c.ENOMEM) return .out_of_memory
return .path_failed
}
length := usize(0)
while (length < buffer.len and buffer[length] != 0) length += 1
if (length == buffer.len) return .path_failed
owned :: try mem.alloc(u8, allocator, length)
memcopy!(owned, buffer[..length])
return owned
}
#! follows symlinks; nonexistent paths (including dangling links) return missing.
#! temporary path storage uses `allocator` and is released before returning.
kind proc(allocator mem.Allocator, path []u8) Kind ! Error {
input :: try terminated(allocator, path)
defer mem.free(allocator, input)
result [1]mut c.Stat := undefined
if (c.stat(ptrcast!(input.ptr), result[..].ptr) == 0) {
mode :: result[0].st_mode & c_ushort(61440)
if (mode == 16384) return .directory
if (mode == 32768) return .file
return .other
}
error :: c.__error()?^
if (error == c.ENOMEM) return .out_of_memory
if (error == c.ENOENT or error == c.ENOTDIR) return .missing
return .path_failed
}
exists proc(allocator mem.Allocator, path []u8) bool ! Error {
return (try kind(allocator, path)) != .missing
}
#! opens an owned directory handle; `path` is borrowed only during this call.
#! temporary path storage uses `allocator` and is released before returning.
#! release the returned handle with close_directory.
open_directory proc(allocator mem.Allocator, path []u8) Directory ! Error {
input :: try terminated(allocator, path)
defer mem.free(allocator, input)
handle :: c.opendir(ptrcast!(input.ptr))
if (handle == null) {
if (c.__error()?^ == c.ENOMEM) return .out_of_memory
return .directory_failed
}
return Directory{ handle = handle }
}
#! returns the next borrowed entry, omitting . and ..; null marks the end.
#! order is filesystem-defined. no allocation is performed; each name expires on the next call.
next proc(directory @Directory) ?Entry ! Error {
if (directory.handle == null) return .directory_failed
while true {
c.__error()?^ = 0
item :: c.readdir(directory.handle)
if (item == null) {
if (c.__error()?^ == c.ENOMEM) return .out_of_memory
if (c.__error()?^ != 0) return .directory_failed
end ?Entry :: null
return end
}
entry :: item?
length :: usize(entry.d_namlen)
if (length >= 1024) return .directory_failed
pointer *u8 :: ptrcast!(entry.d_name[..].ptr)
name :: pointer[..length]
if (mem.eql(name, ".") or mem.eql(name, "..")) continue
entry_kind Kind :: match entry.d_type {
0: .unknown
4: .directory
8: .file
10: .link
else: .other
}
return Entry{ name = name, kind = entry_kind }
}
}
#! invalidates the handle, including when native close reports failure.
close_directory proc(directory @mut Directory) void ! Error {
handle :: directory.handle
if (handle == null) return .directory_failed
directory.handle = null
if (c.closedir(handle) != 0) return .directory_failed
}
#! reads a regular file through runtime IO, accepting at most `maximum` bytes.
#! follows symlinks and rejects devices/FIFOs before opening; metadata uses the host filesystem.
#! returned storage belongs to the caller and retains `allocator`; release with arraylist.deinit. all errors close the file.
#! temporary path storage uses `allocator` and is released before returning.
read_file proc(
allocator mem.Allocator,
runtime io.Io,
path []u8,
maximum usize,
) arraylist.ArrayList(u8) ! ReadFileError {
if (try kind(allocator, path) != .file) return .open_failed
input :: try terminated(allocator, path)
defer mem.free(allocator, input)
file :: try io.open(runtime, input.ptr[..path.len;0], .read_only)
closing := false
defer { if (!closing) io.close(file) catch |_| {} }
bytes := try io.read_all(allocator, io.reader(file), maximum)
errdefer arraylist.deinit(&bytes)
closing = true
try io.close(file)
return bytes
}