Files
honey/source/loader/loader.hon
T

242 lines
8.6 KiB
Plaintext

import "@ffi/c"
import "@std/mem"
import "@std/io"
import "@std/arraylist"
PathError :: enum { invalid_path, path_failed, directory_failed, source_too_large }
Error :: alias mem.AllocError | PathError | io.OpenError | io.CloseError | io.ReadError
@hide:file
terminated proc(allocator mem.Allocator, text []u8) []mut u8 ! (mem.AllocError | PathError) {
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
}
@hide:file
copy proc(allocator mem.Allocator, text []u8) []mut u8 ! mem.AllocError {
bytes :: try mem.alloc(u8, allocator, text.len)
memcopy!(bytes, text)
return bytes
}
#! caller owns the canonical absolute path; release it with mem.free using `allocator`.
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
return try copy(allocator, buffer[..length])
}
@hide:file
mode proc(allocator mem.Allocator, path []u8) c_ushort ! 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)
return result[0].st_mode
error :: c.__error()?^
if (error == c.ENOMEM) return .out_of_memory
if (error == c.ENOENT or error == c.ENOTDIR) return 0
return .path_failed
}
is_directory proc(allocator mem.Allocator, path []u8) bool ! Error {
return ((try mode(allocator, path)) & c_ushort(61440)) == 16384
}
#! returns an owned canonical package directory for either a file or directory input.
directory proc(allocator mem.Allocator, path []u8) []mut u8 ! Error {
resolved :: try canonical(allocator, path)
errdefer mem.free(allocator, resolved)
if (try is_directory(allocator, resolved)) return resolved
end := resolved.len
while (end > 1 and resolved[end - 1] != '/') end -= 1
parent_text :: if (end == 1) resolved[..1] else resolved[..end - 1]
parent :: try copy(allocator, parent_text)
mem.free(allocator, resolved)
return parent
}
@hide:file
join proc(allocator mem.Allocator, base, name []u8) []mut u8 ! mem.AllocError {
separator :: if (base.len == 1 and base[0] == '/') usize(0) else usize(1)
if (base.len > maxval!(usize) - separator or name.len > maxval!(usize) - base.len - separator)
return .out_of_memory
bytes :: try mem.alloc(u8, allocator, base.len + separator + name.len)
memcopy!(bytes[..base.len], base)
if (separator != 0) bytes[base.len] = '/'
memcopy!(bytes[base.len + separator..], name)
return bytes
}
@hide:file
exists proc(allocator mem.Allocator, base, name []u8) bool ! Error {
path :: try join(allocator, base, name)
defer mem.free(allocator, path)
input :: try terminated(allocator, path)
defer mem.free(allocator, input)
if (c.access(ptrcast!(input.ptr), 0) == 0) return true
error :: c.__error()?^
if (error == c.ENOENT or error == c.ENOTDIR) return false
return .path_failed
}
#! finds the nearest ancestor build.hon/build.bro, otherwise uses the supplied directory.
project_root proc(allocator mem.Allocator, package_dir []u8) []mut u8 ! Error {
end := package_dir.len
while true {
base :: package_dir[..end]
if (try exists(allocator, base, "build.hon") or try exists(allocator, base, "build.bro"))
return try copy(allocator, base)
if (end == 1) break
while (end > 1 and package_dir[end - 1] != '/') end -= 1
if (end > 1) end -= 1
}
return try copy(allocator, package_dir)
}
#! relative imports start at the importing package; @ imports start at the project root.
resolve proc(allocator mem.Allocator, root_dir, package_dir, import_path []u8) []mut u8 ! Error {
if (import_path.len == 0 or import_path[0] == '/') return .invalid_path
for (import_path) |byte| if (byte == 0) return .invalid_path
base := package_dir
path := import_path
if (path[0] == '@') {
base = root_dir
path = path[1..]
if (path.len == 0 or path[0] == '/') return .invalid_path
}
joined :: try join(allocator, base, path)
defer mem.free(allocator, joined)
return try canonical(allocator, joined)
}
@hide:file
ends_with proc(text, suffix []u8) bool {
return text.len >= suffix.len and mem.eql(text[text.len - suffix.len..], suffix)
}
@hide:file
before proc(left, right []u8) bool {
count :: if (left.len < right.len) left.len else right.len
for 0..count |index| {
if (left[index] != right[index]) return left[index] < right[index]
}
return left.len < right.len
}
#! lists production sources in bytewise order; caller frees every path and the list.
files proc(allocator mem.Allocator, package_dir []u8) arraylist.ArrayList([]u8) ! Error {
input :: try terminated(allocator, package_dir)
defer mem.free(allocator, input)
opened :: c.opendir(ptrcast!(input.ptr))
if (opened == null) {
if (c.__error()?^ == c.ENOMEM) return .out_of_memory
return .directory_failed
}
handle :: opened?
closing := false
defer { if (!closing) _ = c.closedir(handle) }
result := arraylist.init([]u8, allocator)
errdefer {
for (result.items) |path| mem.free(allocator, path)
arraylist.deinit(&result)
}
while true {
c.__error()?^ = 0
next :: c.readdir(handle)
if (next == null) {
if (c.__error()?^ != 0) return .directory_failed
break
}
entry :: next?
# DT_REG, DT_LNK and DT_UNKNOWN; never open directories, sockets or FIFOs as sources.
if (entry.d_type != 8 and entry.d_type != 10 and entry.d_type != 0) continue
length :: usize(entry.d_namlen)
if (length >= 1024) return .directory_failed
pointer *u8 :: ptrcast!(entry.d_name[..].ptr)
name :: pointer[..length]
if ((!ends_with(name, ".hon") and !ends_with(name, ".bro"))
or ends_with(name, ".test.hon") or ends_with(name, ".test.bro")) continue
path :: try join(allocator, package_dir, name)
errdefer mem.free(allocator, path)
if (entry.d_type != 8 and ((try mode(allocator, path)) & c_ushort(61440)) != 32768) {
mem.free(allocator, path)
continue
}
try arraylist.append(&result, path)
}
closing = true
if (c.closedir(handle) != 0) return .directory_failed
# in-place heapsort: deterministic O(n log n), no additional allocation.
count := result.items.len
start :: divtrunc!(count, 2)
for (0..start) |i| sift(result.items, start - i - 1, count)
while (count > 1) {
count -= 1
temporary :: result.items[0]
result.items[0] = result.items[count]
result.items[count] = temporary
sift(result.items, 0, count)
}
return result
}
@hide:file
sift proc(items []mut []u8, start, count usize) void {
root := start
while (root < divtrunc!(count, 2)) {
child := root * 2 + 1
if (child + 1 < count and before(items[child], items[child + 1])) child += 1
if (!before(items[root], items[child])) break
temporary :: items[root]
items[root] = items[child]
items[child] = temporary
root = child
}
}
#! reads through the supplied IO provider; returned bytes belong to the caller.
read_source proc(allocator mem.Allocator, runtime io.Io, path []u8) arraylist.ArrayList(u8) ! Error {
# source input must be a regular file, not a FIFO/device that can block before reading.
if (((try mode(allocator, path)) & c_ushort(61440)) != 32768) 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 := io.read_all(allocator, io.reader(file), usize(maxval!(u32))) catch |err| {
match err {
.limit_exceeded: return .source_too_large
else: return err
}
}
errdefer arraylist.deinit(&bytes)
closing = true
try io.close(file)
return bytes
}