242 lines
8.6 KiB
Plaintext
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
|
|
}
|