hare

The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

commit f2f0ca8d0da25116c0d313bae6ae1a7603930cfa
parent 36a475c57d23f9b04f9e0a2024d3647419095bf9
Author: Drew DeVault <sir@cmpwn.com>
Date:   Wed, 10 Mar 2021 10:10:21 -0500

all: use str for path components

On Linux, and other operating systems, path components can be any
sequence of bytes other than ASCII '/', and the stdlib API was
originally designed to accomodate this.

However, this is ultimately quite cumbersome. Some examples:

- argv can also be non-UTF-8, so os::args has to be updated as well,
  causing almost all Hare programs to deal with this dumb edge case
- Ditto for environment variables.
- Ditto for os::exec.
- Filenames can also contain \0, which breaks so much shit. Hare handles
  this as str but not as *const char.
- path::path cannot be passed to fmt for printing without lengthly
  special-case handling

This is a bit of unnecessary complexity that permeates huge swaths of
the stdlib and trickles into end-user programs. It simplifies things
greatly for everyone and for 99% of use-cases if we just abort when we
encounter non-UTF-8 paths.

That said, some programs do have a legitimate reason to support
non-UTF-8 paths, arguments, or environment variables. For this reason, I
have left the rt API intact, which understands both strings and byte
slices. Any program which has a genuine need to support this can write
syscalls or access rt's initialization vector directly. This is
non-portable - but that was a cost they were likely to bear regardless.

Diffstat:
Mdirs/xdg.ha | 10++++------
Mfs/fs.ha | 41++++++++++++++++++-----------------------
Mfs/types.ha | 40+++++++++++++++-------------------------
Mfs/util.ha | 6+++---
Mhare/module/context.ha | 20++++++++++----------
Mhare/module/scan.ha | 32++++++++++++++++----------------
Mhare/module/types.ha | 5++---
Mos/+linux/dirfdfs.ha | 52++++++++++++++++++----------------------------------
Mos/+linux/fs.ha | 19+++++++------------
Mos/+linux/open.ha | 8+++-----
Mos/fs.ha | 24++++++++++--------------
Mpath/iter.ha | 39++++++++++++++++-----------------------
Mpath/join.ha | 44++++++++++++++++++++------------------------
Mpath/names.ha | 62++++++++++++++++++++++----------------------------------------
Dpath/norm.ha | 6------
Mpath/util.ha | 34++--------------------------------
Mtemp/+linux.ha | 6+++---
17 files changed, 169 insertions(+), 279 deletions(-)

diff --git a/dirs/xdg.ha b/dirs/xdg.ha @@ -3,7 +3,7 @@ use os; use path; use io; -fn lookup(prog: str, var: str, default: path::path) path::path = { +fn lookup(prog: str, var: str, default: str) str = { match (os::getenv(var)) { s: str => { let path = path::join(s, prog); @@ -34,8 +34,7 @@ fn lookup(prog: str, var: str, default: path::path) path::path = { // Returns a directory suitable for storing config files. If 'prog' is given, a // unique path for this program to store data will be returned. -export fn config(prog: str) path::path = - lookup(prog, "XDG_CONFIG_HOME", ".config"); +export fn config(prog: str) str = lookup(prog, "XDG_CONFIG_HOME", ".config"); // Returns an [fs::fs] for storing config files. If 'prog' is given, a unique // path for this program to store data will be returned. @@ -43,8 +42,7 @@ export fn config_fs(prog: str) *fs::fs = os::diropen(config(prog)) as *fs::fs; // Returns a directory suitable for cache files. If 'prog' is given, a unique // path for this program to store data will be returned. -export fn cache(prog: str) path::path = - lookup(prog, "XDG_CACHE_HOME", ".cache"); +export fn cache(prog: str) str = lookup(prog, "XDG_CACHE_HOME", ".cache"); // Returns an [fs::fs] for cache files. If 'prog' is given, a unique path for // this program to store data will be returned. @@ -52,7 +50,7 @@ export fn cache_fs(prog: str) *fs::fs = os::diropen(cache(prog)) as *fs::fs; // Returns a directory suitable for persistent data files. If 'prog' is given, a // unique path for this program to store data will be returned. -export fn data(prog: str) path::path = +export fn data(prog: str) str = lookup(prog, "XDG_DATA_HOME", path::join(".local", "share")); // Returns an [fs::fs] for persistent data files. If 'prog' is given, a unique diff --git a/fs/fs.ha b/fs/fs.ha @@ -11,11 +11,7 @@ export fn close(fs: *fs) void = { // Opens a file. If no flags are provided, the default read/write mode is // RDONLY. -export fn open( - fs: *fs, - path: path::path, - flags: flags... -) (*io::stream | error) = { +export fn open(fs: *fs, path: str, flags: flags...) (*io::stream | error) = { return match (fs.open) { null => io::unsupported, f: *openfunc => f(fs, path, flags...), @@ -29,7 +25,7 @@ export fn open( // are discarded. export fn create( fs: *fs, - path: path::path, + path: str, mode: mode, flags: flags... ) (*io::stream | error) = { @@ -41,7 +37,7 @@ export fn create( }; // Removes a file. -export fn remove(fs: *fs, path: path::path) (void | error) = { +export fn remove(fs: *fs, path: str) (void | error) = { return match (fs.remove) { null => io::unsupported, f: *removefunc => f(fs, path), @@ -51,7 +47,7 @@ export fn remove(fs: *fs, path: path::path) (void | error) = { // Returns an iterator for a path, which yields the contents of a directory. // Pass empty string to yield from the root. The order in which entries are // returned is undefined. -export fn iter(fs: *fs, path: path::path) (*iterator | error) = { +export fn iter(fs: *fs, path: str) (*iterator | error) = { return match (fs.iter) { null => io::unsupported, f: *iterfunc => f(fs, path), @@ -60,7 +56,7 @@ export fn iter(fs: *fs, path: path::path) (*iterator | error) = { // Obtains information about a file or directory. If the target is a symlink, // information is returned about the link, not its target. -export fn stat(fs: *fs, path: path::path) (filestat | error) = { +export fn stat(fs: *fs, path: str) (filestat | error) = { return match (fs.stat) { null => io::unsupported, f: *statfunc => f(fs, path), @@ -70,7 +66,7 @@ export fn stat(fs: *fs, path: path::path) (filestat | error) = { // Opens a new filesystem for a subdirectory. The subdirectory must be closed // separately from the parent filesystem, and its lifetime can outlive that of // its parent. -export fn subdir(fs: *fs, path: path::path) (*fs | error) = { +export fn subdir(fs: *fs, path: str) (*fs | error) = { return match (fs.subdir) { null => io::unsupported, f: *subdirfunc => f(fs, path), @@ -78,7 +74,7 @@ export fn subdir(fs: *fs, path: path::path) (*fs | error) = { }; // Creates a directory. -export fn mkdir(fs: *fs, path: path::path) (void | error) = { +export fn mkdir(fs: *fs, path: str) (void | error) = { return match (fs.mkdir) { null => io::unsupported, f: *mkdirfunc => f(fs, path), @@ -86,9 +82,9 @@ export fn mkdir(fs: *fs, path: path::path) (void | error) = { }; // Makes a directory, and all non-extant directories in its path. -export fn mkdirs(fs: *fs, path: path::path) (void | error) = { +export fn mkdirs(fs: *fs, path: str) (void | error) = { let parent = path::dirname(path); - if (!path::equal(path, parent)) { + if (path != parent) { match (mkdirs(fs, parent)) { exists => void, err: error => return err, @@ -100,7 +96,7 @@ export fn mkdirs(fs: *fs, path: path::path) (void | error) = { // Removes a directory. The target directory must be empty; see [rmdirall] to // remove its contents as well. -export fn rmdir(fs: *fs, path: path::path) (void | error) = { +export fn rmdir(fs: *fs, path: str) (void | error) = { return match (fs.rmdir) { null => io::unsupported, f: *rmdirfunc => f(fs, path), @@ -108,23 +104,22 @@ export fn rmdir(fs: *fs, path: path::path) (void | error) = { }; // Removes a directory, and anything in it. -export fn rmdirall(fs: *fs, path: path::path) (void | error) = { +export fn rmdirall(fs: *fs, path: str) (void | error) = { let it = iter(fs, path)?; for (true) match (next(it)) { ent: dirent => { - if (path::equal(ent.name, ".") - || path::equal(ent.name, "..")) { + if (ent.name == "." || ent.name == "..") { continue; }; switch (ent.ftype) { mode::DIR => { let p = path::join(path, ent.name); - defer path::path_free(p); + defer free(p); rmdirall(fs, p)?; }, * => { let p = path::join(path, ent.name); - defer path::path_free(p); + defer free(p); remove(fs, p)?; }, }; @@ -136,7 +131,7 @@ export fn rmdirall(fs: *fs, path: path::path) (void | error) = { // Creates a directory and returns a subdir for it. Some filesystems support // doing this operation atomically, but if not, a fallback is used. -export fn mksubdir(fs: *fs, path: path::path) (*fs | error) = { +export fn mksubdir(fs: *fs, path: str) (*fs | error) = { return match (fs.mksubdir) { null => { mkdir(fs, path)?; @@ -147,9 +142,9 @@ export fn mksubdir(fs: *fs, path: path::path) (*fs | error) = { }; // Resolves a path to its absolute, normalized value. This consoldates ./ and -// ../ sequences, roots the path, and returns a new [path::path]. The caller -// must free the return value. -export fn resolve(fs: *fs, path: path::path) path::path = { +// ../ sequences, roots the path, and returns a new path. The caller must free +// the return value. +export fn resolve(fs: *fs, path: str) str = { match (fs.resolve) { f: *resolvefunc => return f(fs, path), null => void, diff --git a/fs/types.ha b/fs/types.ha @@ -113,31 +113,21 @@ export type filestat = struct { export type dirent = struct { // The name of this entry. Not fully qualified: for example, // "foo/bar/baz.txt" would store "baz.txt" here. - name: path::path, + name: str, // The type of this entry. The permission bits may be unset. ftype: mode, }; // Duplicates a [dirent] object. Call [dirent_free] to get rid of it later. -export fn dirent_dup(e: dirent) dirent = { - let new = e; - new.name = match (new.name) { - s: str => strings::dup(s), - b: []u8 => { - let n: []u8 = []; - append(n, ...b); - n; - }, - }; +export fn dirent_dup(e: *dirent) dirent = { + let new = *e; + new.name = strings::dup(e.name); return new; }; // Frees a [dirent] object which was duplicated with [dirent_dup]. -export fn dirent_free(e: dirent) void = match (e.name) { - s: str => free(s), - b: []u8 => free(b), -}; +export fn dirent_free(e: *dirent) void = free(e.name); // Flags to use for opening a file. Not all operating systems support all flags; // at a minimum, RDONLY, WRONLY, RDWR, and CREATE will be supported. @@ -161,24 +151,24 @@ export type flags = enum int { }; export type closefunc = fn(fs: *fs) void; -export type removefunc = fn(fs: *fs, path: path::path) (void | error); -export type iterfunc = fn(fs: *fs, path: path::path) (*iterator | error); -export type statfunc = fn(fs: *fs, path: path::path) (filestat | error); -export type subdirfunc = fn(fs: *fs, path: path::path) (*fs | error); -export type mkdirfunc = fn(fs: *fs, path: path::path) (void | error); -export type rmdirfunc = fn(fs: *fs, path: path::path) (void | error); -export type mksubdirfunc = fn(fs: *fs, path: path::path) (*fs | error); -export type resolvefunc = fn(fs: *fs, path: path::path) path::path; +export type removefunc = fn(fs: *fs, path: str) (void | error); +export type iterfunc = fn(fs: *fs, path: str) (*iterator | error); +export type statfunc = fn(fs: *fs, path: str) (filestat | error); +export type subdirfunc = fn(fs: *fs, path: str) (*fs | error); +export type mkdirfunc = fn(fs: *fs, path: str) (void | error); +export type rmdirfunc = fn(fs: *fs, path: str) (void | error); +export type mksubdirfunc = fn(fs: *fs, path: str) (*fs | error); +export type resolvefunc = fn(fs: *fs, path: str) str; export type openfunc = fn( fs: *fs, - path: path::path, + path: str, flags: flags... ) (*io::stream | error); export type createfunc = fn( fs: *fs, - path: path::path, + path: str, mode: mode, flags: flags... ) (*io::stream | error); diff --git a/fs/util.ha b/fs/util.ha @@ -78,12 +78,12 @@ export fn is_socket(mode: mode) bool = mode & mode::SOCK == mode::SOCK; // Reads all entries from a directory. The caller must free the return value // with [dirents_free]. -export fn readdir(fs: *fs, path: path::path) ([]dirent | error) = { +export fn readdir(fs: *fs, path: str) ([]dirent | error) = { let i = iter(fs, path)?; let ents: []dirent = []; for (true) { match (next(i)) { - d: dirent => append(ents, dirent_dup(d)), + d: dirent => append(ents, dirent_dup(&d)), void => break, }; }; @@ -93,6 +93,6 @@ export fn readdir(fs: *fs, path: path::path) ([]dirent | error) = { // Frees a slice of [dirent]s. export fn dirents_free(d: []dirent) void = { for (let i = 0z; i < len(d); i += 1) { - dirent_free(d[i]); + dirent_free(&d[i]); }; }; diff --git a/hare/module/context.ha b/hare/module/context.ha @@ -13,10 +13,10 @@ export type context = struct { fs: *fs::fs, // List of paths to search, generally populated from HAREPATH plus some // baked-in default. - paths: []path::path, + paths: []str, // Path to the Hare cache, generally populated from HARECACHE and // defaulting to $XDG_CACHE_HOME/hare. - cache: path::path, + cache: str, // Build tags to apply to this context. tags: []tag, }; @@ -26,9 +26,9 @@ export fn context_init(tags: []tag) context = { let ctx = context { fs = os::cwd, tags = tags, - paths: []path::path = match (os::getenv("HAREPATH")) { + paths: []str = match (os::getenv("HAREPATH")) { void => { - let path: []path::path = alloc([ + let path: []str = alloc([ DEFAULT_HAREPATH, dirs::data("hare"), ]); @@ -36,7 +36,7 @@ export fn context_init(tags: []tag) context = { }, s: str => { let sl = strings::split(s, ":"); - let path: []path::path = alloc([], len(sl) + 1); + let path: []str = alloc([], len(sl) + 1); let i = 0z; for (i < len(sl); i += 1) { append(path, sl[i]); @@ -46,7 +46,7 @@ export fn context_init(tags: []tag) context = { path; }, }, - cache: path::path = match (os::getenv("HARECACHE")) { + cache: str = match (os::getenv("HARECACHE")) { void => dirs::cache("hare"), s: str => s, }, @@ -57,11 +57,11 @@ export fn context_init(tags: []tag) context = { // Converts an identifier to a partial path (e.g. foo::bar becomes foo/bar). The // return value must be freed by the caller. -export fn ident_path(name: ast::ident) path::path = { +export fn ident_path(name: ast::ident) str = { let p = path::join(name[0]); for (let i = 1z; i < len(name); i += 1) { let q = path::join(p, name[i]); - path::path_free(p); + free(p); p = q; }; return p; @@ -70,6 +70,6 @@ export fn ident_path(name: ast::ident) path::path = { @test fn ident_path() void = { let ident: ast::ident = ["foo", "bar", "baz"]; let p = ident_path(ident); - defer path::path_free(p); - assert(path::equal(p, "foo/bar/baz")); + defer free(p); + assert(p == "foo/bar/baz"); }; diff --git a/hare/module/scan.ha b/hare/module/scan.ha @@ -15,7 +15,7 @@ use strio; // Scans the files in a directory for eligible build inputs and returns a // [version] which includes all applicable files and their dependencies. -export fn scan(ctx: *context, path: path::path) (version | error) = { +export fn scan(ctx: *context, path: str) (version | error) = { let sha = sha256::sha256(); //defer! hash::close(sha); let iter = match (fs::iter(ctx.fs, path)) { @@ -39,7 +39,7 @@ export fn scan(ctx: *context, path: path::path) (version | error) = { hash::write(sha, in.hash); return version { hash = hash::finish(sha), - basedir = path::dup(path::dirname(path)), + basedir = strings::dup(path::dirname(path)), depends = deps, inputs = inputs, }; @@ -48,7 +48,7 @@ export fn scan(ctx: *context, path: path::path) (version | error) = { iter: *fs::iterator => iter, }; let ver = version { - basedir = path::dup(path), + basedir = strings::dup(path), ... }; scan_directory(ctx, &ver, sha, path, iter)?; @@ -60,7 +60,7 @@ fn scan_directory( ctx: *context, ver: *version, sha: *hash::hash, - path: path::path, + path: str, iter: *fs::iterator, ) (void | error) = { for (true) match (fs::next(iter)) { @@ -68,9 +68,10 @@ fn scan_directory( ent: fs::dirent => switch (ent.ftype) { fs::mode::LINK => abort(), // TODO fs::mode::DIR => { - let d = ent.name: []u8; - if (len(d) == 0 || (d[0] != '+': u32: u8 - && d[0] != '-': u32: u8)) { + let d = strings::to_utf8(ent.name); + if (len(d) == 0 || ( + !strings::has_prefix(ent.name, "+") && + !strings::has_prefix(ent.name, "-"))) { continue; }; if (!eligible(ctx, ent.name, true)) { @@ -104,7 +105,7 @@ export fn lookup(ctx: *context, name: ast::ident) (version | error) = { let ipath = ident_path(name); for (let i = len(ctx.paths); i > 0; i -= 1) { let cand = path::join(ctx.paths[i - 1], ipath); - defer path::path_free(cand); + defer free(cand); match (scan(ctx, cand)) { v: version => return v, e: error => void, @@ -113,10 +114,9 @@ export fn lookup(ctx: *context, name: ast::ident) (version | error) = { return module_not_found; }; -fn eligible(ctx: *context, name: path::path, dir: bool) bool = { - if (!utf8::valid(name: []u8)) { - return false; - }; +fn eligible(ctx: *context, name: str, dir: bool) bool = { + // TODO: Rewrite me to use string manipulation rather than byte slices + // This pre-dates the change from path as (str | []u8) to just str if (!dir) { let eligible = false; const ext = path::extension(name); @@ -134,7 +134,7 @@ fn eligible(ctx: *context, name: path::path, dir: bool) bool = { // XXX: It might be nice if the stdlib offered search functions which // support multiple needles - let b = name: []u8; + let b = strings::to_utf8(name); let p = bytes::index(b, '+': u32: u8); let m = bytes::index(b, '-': u32: u8); if (p is void && m is void) { @@ -157,7 +157,7 @@ fn eligible(ctx: *context, name: path::path, dir: bool) bool = { return tags_compat(ctx.tags, tags); }; -fn type_for_ext(name: path::path) (filetype | void) = { +fn type_for_ext(name: str) (filetype | void) = { const ext = path::extension(name); return if (ext == ".ha") filetype::HARE @@ -167,7 +167,7 @@ fn type_for_ext(name: path::path) (filetype | void) = { fn scan_file( ctx: *context, - path: path::path, + path: str, deps: *[]ast::ident, ) ([]u8 | error) = { let f = fs::open(ctx.fs, path)?; @@ -177,7 +177,7 @@ fn scan_file( let tee = io::tee(f, hash::writer(sha)); defer io::close(tee); - let lexer = lex::init(tee, path as str); + let lexer = lex::init(tee, path); let imports = parse::imports(&lexer)?; for (let i = 0z; i < len(imports); i += 1) { let ident = match (imports[i]) { diff --git a/hare/module/types.ha b/hare/module/types.ha @@ -2,7 +2,6 @@ use fs; use hare::ast; use hare::parse; use io; -use path; // The inclusive/exclusive state for a build tag. export type tag_mode = enum { @@ -26,7 +25,7 @@ export type manifest = struct { // A module version: a set of possible input files for that module. export type version = struct { hash: []u8, - basedir: path::path, + basedir: str, depends: []ast::ident, inputs: []input, }; @@ -39,7 +38,7 @@ export type filetype = enum { // An input to a module, generally a source file. export type input = struct { hash: []u8, - path: path::path, + path: str, ft: filetype, stat: fs::filestat, }; diff --git a/os/+linux/dirfdfs.ha b/os/+linux/dirfdfs.ha @@ -92,7 +92,7 @@ export fn dirfs_clone(fs: *fs::fs, resolve: resolve...) *fs::fs = { fn _fs_open( fs: *fs::fs, - path: path::path, + path: str, mode: io::mode, oh: *rt::open_how, ) (*io::stream | fs::error) = { @@ -112,22 +112,17 @@ fn _fs_open( oh.resolve |= rt::RESOLVE_NO_XDEV; }; - const name: str = match (path) { - s: str => s, - b: []u8 => "<open([]u8)>", - }; - let fd = match (rt::openat2(fs.dirfd, path, oh, size(rt::open_how))) { err: rt::errno => return errno_to_io(err), fd: int => fd, }; - return fdopen(fd, name, mode); + return fdopen(fd, path, mode); }; fn fs_open( fs: *fs::fs, - path: path::path, + path: str, flags: fs::flags... ) (*io::stream | fs::error) = { let oflags = 0; @@ -158,7 +153,7 @@ fn fs_open( fn fs_create( fs: *fs::fs, - path: path::path, + path: str, mode: fs::mode, flags: fs::flags... ) (*io::stream | fs::error) = { @@ -190,7 +185,7 @@ fn fs_create( return _fs_open(fs, path, iomode, &oh); }; -fn fs_remove(fs: *fs::fs, path: path::path) (void | fs::error) = { +fn fs_remove(fs: *fs::fs, path: str) (void | fs::error) = { let fs = fs: *os_filesystem; match (rt::unlinkat(fs.dirfd, path, 0)) { err: rt::errno => return errno_to_io(err), @@ -198,7 +193,7 @@ fn fs_remove(fs: *fs::fs, path: path::path) (void | fs::error) = { }; }; -fn fs_stat(fs: *fs::fs, path: path::path) (fs::filestat | fs::error) = { +fn fs_stat(fs: *fs::fs, path: str) (fs::filestat | fs::error) = { let fs = fs: *os_filesystem; let st = rt::st { ... }; match (rt::fstatat(fs.dirfd, path, &st, rt::AT_SYMLINK_NOFOLLOW)) { @@ -218,7 +213,7 @@ fn fs_stat(fs: *fs::fs, path: path::path) (fs::filestat | fs::error) = { }; }; -fn fs_subdir(fs: *fs::fs, path: path::path) (*fs::fs | fs::error) = { +fn fs_subdir(fs: *fs::fs, path: str) (*fs::fs | fs::error) = { let fs = fs: *os_filesystem; let oh = rt::open_how { flags = (rt::O_RDONLY | rt::O_CLOEXEC | rt::O_DIRECTORY): u64, @@ -234,7 +229,7 @@ fn fs_subdir(fs: *fs::fs, path: path::path) (*fs::fs | fs::error) = { return dirfdopen(fd); }; -fn fs_rmdir(fs: *fs::fs, path: path::path) (void | fs::error) = { +fn fs_rmdir(fs: *fs::fs, path: str) (void | fs::error) = { let fs = fs: *os_filesystem; match (rt::unlinkat(fs.dirfd, path, rt::AT_REMOVEDIR)) { err: rt::errno => return errno_to_io(err), @@ -242,7 +237,7 @@ fn fs_rmdir(fs: *fs::fs, path: path::path) (void | fs::error) = { }; }; -fn fs_mkdir(fs: *fs::fs, path: path::path) (void | fs::error) = { +fn fs_mkdir(fs: *fs::fs, path: str) (void | fs::error) = { let fs = fs: *os_filesystem; return match (rt::mkdirat(fs.dirfd, path, 0o755)) { err: rt::errno => switch (err) { @@ -253,16 +248,11 @@ fn fs_mkdir(fs: *fs::fs, path: path::path) (void | fs::error) = { }; }; -fn resolve_part(parts: *[]path::path, part: path::path) void = { - const dot = strings::to_utf8("."), dotdot = strings::to_utf8(".."); - let b = match (part) { - b: []u8 => b, - s: str => strings::to_utf8(s), - }; - if (bytes::equal(b, dot)) { +fn resolve_part(parts: *[]str, part: str) void = { + if (part == ".") { // no-op void; - } else if (bytes::equal(b, dotdot)) { + } else if (part == "..") { // XXX: We should not have to dereference this if (len(*parts) != 0) { delete(parts[len(*parts) - 1]); @@ -272,19 +262,19 @@ fn resolve_part(parts: *[]path::path, part: path::path) void = { }; }; -fn fs_resolve(fs: *fs::fs, path: path::path) path::path = { - let parts: []path::path = []; +fn fs_resolve(fs: *fs::fs, path: str) str = { + let parts: []str = []; if (!path::abs(path)) { let iter = path::iter(getcwd()); for (true) match (path::next(&iter)) { void => break, - p: path::path => resolve_part(&parts, p), + p: str => resolve_part(&parts, p), }; }; let iter = path::iter(path); for (true) match (path::next(&iter)) { void => break, - p: path::path => resolve_part(&parts, p), + p: str => resolve_part(&parts, p), }; return path::join(parts...); }; @@ -305,7 +295,7 @@ type os_iterator = struct { buf: [BUFSIZ]u8, }; -fn fs_iter(fs: *fs::fs, path: path::path) (*fs::iterator | fs::error) = { +fn fs_iter(fs: *fs::fs, path: str) (*fs::iterator | fs::error) = { let fs = fs: *os_filesystem; let oh = rt::open_how { flags = (rt::O_RDONLY | rt::O_CLOEXEC | rt::O_DIRECTORY): u64, @@ -346,13 +336,7 @@ fn iter_next(iter: *fs::iterator) (fs::dirent | void) = { }; let de = &iter.buf[iter.buf_pos]: *rt::dirent64; iter.buf_pos += de.d_reclen; - let ln = strings::c_strlen(&de.d_name: *const char); - let name = &de.d_name: *[*]u8; - let name = if (!utf8::valid(name[..ln])) { - name[..ln]; - } else { - strings::from_utf8_unsafe(name[..ln]); - }; + let name = strings::from_c(&de.d_name: *const char); let ftype: fs::mode = switch (de.d_type) { rt::DT_UNKNOWN => fs::mode::UNKNOWN, diff --git a/os/+linux/fs.ha b/os/+linux/fs.ha @@ -17,11 +17,11 @@ use strings; // Returns the current working directory. The return value is statically // allocated and must be duplicated (see [strings::dup]) before calling getcwd // again. -export fn getcwd() path::path = strings::from_c(rt::getcwd() as *const char); +export fn getcwd() str = strings::from_c(rt::getcwd() as *const char); // Change the current working directory. -export fn chdir(target: (*fs::fs | ...path::path)) (void | fs::error) = { - const path: []u8 = match (target) { +export fn chdir(target: (*fs::fs | str)) (void | fs::error) = { + const path: str = match (target) { fs: *fs::fs => { assert(fs.open == &fs_open); let fs = fs: *os_filesystem; @@ -30,10 +30,9 @@ export fn chdir(target: (*fs::fs | ...path::path)) (void | fs::error) = { void => void, }; }, - s: str => strings::to_utf8(s), - b: []u8 => b, + s: str => s, }; - return match (rt::chdir(path: *[*]u8: *const char)) { + return match (rt::chdir(path)) { err: rt::errno => errno_to_io(err): fs::error, void => void, }; @@ -43,12 +42,8 @@ export fn chdir(target: (*fs::fs | ...path::path)) (void | fs::error) = { // have root or otherwise elevated permissions. // // This function is not appropriate for sandboxing. -export fn chroot(target: path::path) (void | fs::error) = { - const path: []u8 = match (target) { - s: str => strings::to_utf8(s), - b: []u8 => b, - }; - return match (rt::chroot(path: *[*]u8: *const char)) { +export fn chroot(target: str) (void | fs::error) = { + return match (rt::chroot(target)) { err: rt::errno => errno_to_io(err): fs::error, void => void, }; diff --git a/os/+linux/open.ha b/os/+linux/open.ha @@ -8,10 +8,8 @@ use path; // [fs::flags::CLOEXEC] are used when opening the file. If you pass your own // flags, it is recommended that you add the latter two unless you know that you // do not want them. -export fn open( - path: path::path, - flags: fs::flags... -) (*io::stream | fs::error) = fs::open(cwd, path, flags...); +export fn open(path: str, flags: fs::flags...) (*io::stream | fs::error) = + fs::open(cwd, path, flags...); // Creates a new file and opens it for writing. // @@ -23,7 +21,7 @@ export fn open( // Only the permission bits of the mode are used. If other bits are set, they // are discarded. export fn create( - path: path::path, + path: str, mode: fs::mode, flags: fs::flags... ) (*io::stream | fs::error) = fs::create(cwd, path, mode, flags...); diff --git a/os/fs.ha b/os/fs.ha @@ -8,38 +8,34 @@ export let root: *fs::fs = null: *fs::fs; export let cwd: *fs::fs = null: *fs::fs; // Removes a file. -export fn remove(path: path::path) (void | fs::error) = fs::remove(cwd, path); +export fn remove(path: str) (void | fs::error) = fs::remove(cwd, path); // Creates an [fs::iterator] for a given directory to read its contents. -export fn iterdir(path: path::path) (*fs::iterator | fs::error) = - fs::iter(cwd, path); +export fn iterdir(path: str) (*fs::iterator | fs::error) = fs::iter(cwd, path); // Reads all entries from a directory. The caller must free the return value // with [fs::dirents_free]. -export fn readdir(path: path::path) ([]fs::dirent | fs::error) = - fs::readdir(cwd, path); +export fn readdir(path: str) ([]fs::dirent | fs::error) = fs::readdir(cwd, path); // Returns file information for a given path. -export fn stat(path: path::path) (fs::filestat | fs::error) = fs::stat(cwd, path); +export fn stat(path: str) (fs::filestat | fs::error) = fs::stat(cwd, path); // Opens a directory as a filesystem. -export fn diropen(path: path::path) (*fs::fs | fs::error) = fs::subdir(cwd, path); +export fn diropen(path: str) (*fs::fs | fs::error) = fs::subdir(cwd, path); // Creates a directory. -export fn mkdir(path: path::path) (void | fs::error) = fs::mkdir(cwd, path); +export fn mkdir(path: str) (void | fs::error) = fs::mkdir(cwd, path); // Creates a directory, and all non-extant directories in its path. -export fn mkdirs(path: path::path) (void | fs::error) = fs::mkdirs(cwd, path); +export fn mkdirs(path: str) (void | fs::error) = fs::mkdirs(cwd, path); // Removes a directory. The target directory must be empty; see [rmdirall] to // remove its contents as well. -export fn rmdir(path: path::path) (void | fs::error) = fs::rmdir(cwd, path); +export fn rmdir(path: str) (void | fs::error) = fs::rmdir(cwd, path); // Removes a directory, and anything in it. -export fn rmdirall(path: path::path) (void | fs::error) = - fs::rmdirall(cwd, path); +export fn rmdirall(path: str) (void | fs::error) = fs::rmdirall(cwd, path); // Creates a directory and returns a subdir for it. Some filesystems support // doing this operation atomically, but if not, a fallback is used. -export fn mksubdir(path: path::path) (*fs::fs | fs::error) = - fs::mksubdir(cwd, path); +export fn mksubdir(path: str) (*fs::fs | fs::error) = fs::mksubdir(cwd, path); diff --git a/path/iter.ha b/path/iter.ha @@ -1,12 +1,9 @@ use bytes; use strings; -const pathsep: []u8 = [PATHSEP]; - export type iflags = enum uint { NONE = 0, - STRING = 1 << 0, - ABSOLUTE = 1 << 1, + ABSOLUTE = 1 << 0, }; // An iterator which yields each component of a path. @@ -15,15 +12,13 @@ export type iterator = struct { flags: iflags, }; +let pathsep: []u8 = [PATHSEP]; + // Returns an iterator which yields each component of a path. If the path is // absolute, the first component will be the root path (e.g. /). -export fn iter(path: path) iterator = { +export fn iter(path: str) iterator = { let flags = iflags::NONE; - if (path is str) { - flags |= iflags::STRING; - }; - - let pb = pathbytes(path); + let pb = strings::to_utf8(path); if (len(pb) > 0 && pb[0] == PATHSEP) { flags |= iflags::ABSOLUTE; pb = pb[1..]; @@ -39,38 +34,36 @@ export fn iter(path: path) iterator = { }; // Returns the next path component from an iterator, or void if none remain. -export fn next(iter: *iterator) (path | void) = { +export fn next(iter: *iterator) (str | void) = { if (iter.flags & iflags::ABSOLUTE == iflags::ABSOLUTE) { iter.flags &= ~iflags::ABSOLUTE; static assert(PATHSEP <= 0x7F); return strings::from_utf8_unsafe(pathsep); }; return match (bytes::next_token(&iter.tok)) { + b: []u8 => strings::from_utf8_unsafe(b), void => void, - b: []u8 => if (iter.flags & iflags::STRING == iflags::STRING) - strings::from_utf8_unsafe(b) - else b, }; }; @test fn iter() void = { assert(PATHSEP == '/': u32: u8); // meh let i = iter("/foo/bar/baz"); - assert(equal(next(&i) as path, "/")); - assert(equal(next(&i) as path, "foo")); - assert(equal(next(&i) as path, "bar")); - assert(equal(next(&i) as path, "baz")); + assert(next(&i) as str == "/"); + assert(next(&i) as str == "foo"); + assert(next(&i) as str == "bar"); + assert(next(&i) as str == "baz"); assert(next(&i) is void); let i = iter("foo/bar/baz/"); - assert(equal(next(&i) as path, "foo")); - assert(equal(next(&i) as path, "bar")); - assert(equal(next(&i) as path, "baz")); + assert(next(&i) as str == "foo"); + assert(next(&i) as str == "bar"); + assert(next(&i) as str == "baz"); assert(next(&i) is void); let i = iter("foo"); - assert(equal(next(&i) as path, "foo")); + assert(next(&i) as str == "foo"); assert(next(&i) is void); let i = iter("/"); - assert(equal(next(&i) as path, "/")); + assert(next(&i) as str == "/"); assert(next(&i) is void); }; diff --git a/path/join.ha b/path/join.ha @@ -4,16 +4,14 @@ use strings; use io; // Joins together several path components with the path separator. The caller -// must free the return value with [path_free]. -export fn join(paths: path...) path = { +// must free the return value. +export fn join(paths: str...) str = { // TODO: Normalize inputs so that if they end with a / we don't double // up on delimiters let sink = bufio::dynamic(io::mode::WRITE); let utf8 = true; for (let i = 0z; i < len(paths); i += 1) { - utf8 = utf8 && paths[i] is str; - - let buf = pathbytes(paths[i]); + let buf = strings::to_utf8(paths[i]); let l = len(buf); if (l == 0) continue; for (l > 0 && buf[l - 1] == PATHSEP) { @@ -28,42 +26,40 @@ export fn join(paths: path...) path = { }; }; - return - if (utf8) strings::from_utf8_unsafe(bufio::finish(sink)) - else bufio::finish(sink); + return strings::from_utf8_unsafe(bufio::finish(sink)); }; @test fn join() void = { assert(PATHSEP == '/': u32: u8); // TODO: meh let i = join("foo"); - defer path_free(i); - assert(i as str == "foo"); + defer free(i); + assert(i == "foo"); let p = join(i, "bar", "baz"); - defer path_free(p); - assert(p as str == "foo/bar/baz"); + defer free(p); + assert(p == "foo/bar/baz"); let q = join(p, "bat", "bad"); - defer path_free(q); - assert(q as str == "foo/bar/baz/bat/bad"); + defer free(q); + assert(q == "foo/bar/baz/bat/bad"); let r = join(p, q); - defer path_free(r); - assert(r as str == "foo/bar/baz/foo/bar/baz/bat/bad"); + defer free(r); + assert(r == "foo/bar/baz/foo/bar/baz/bat/bad"); let p = join("foo/", "bar"); - defer path_free(p); - assert(p as str == "foo/bar"); + defer free(p); + assert(p == "foo/bar"); let p = join("foo///", "bar"); - defer path_free(p); - assert(p as str == "foo/bar"); + defer free(p); + assert(p == "foo/bar"); let p = join("foo", "", "bar"); - defer path_free(p); - assert(p as str == "foo/bar"); + defer free(p); + assert(p == "foo/bar"); let p = join("/", "foo", "bar", "baz"); - defer path_free(p); - assert(p as str == "/foo/bar/baz"); + defer free(p); + assert(p == "/foo/bar/baz"); }; diff --git a/path/names.ha b/path/names.ha @@ -6,8 +6,8 @@ use strings; // returns the directory in which that file resides. For a path to a directory, // this returns the path to its parent directory. The return value is borrowed // from the input, use [dup] to extend its lifetime. -export fn dirname(path: path::path) path::path = { - let b = pathbytes(normalize(path)); +export fn dirname(path: str) str = { + let b = strings::to_utf8(path); let i = match (bytes::rindex(b, PATHSEP)) { void => return path, z: size => z, @@ -15,76 +15,58 @@ export fn dirname(path: path::path) path::path = { if (i == 0) { i += 1; }; - return - if (path is str) strings::from_utf8_unsafe(b[..i]) - else b[..i]; + return strings::from_utf8_unsafe(b[..i]); }; @test fn dirname() void = { - let p: path = "/foo/bar"; - assert(dirname(p) as str == "/foo"); - let p: path = "/foo"; - assert(dirname(p) as str == "/"); - let p: path = "/"; - assert(dirname(p) as str == "/"); - let p: path = "foo/bar"; - assert(dirname(p) as str == "foo"); - let p: path = "foo"; - assert(dirname(p) as str == "foo"); + assert(dirname("/foo/bar") == "/foo"); + assert(dirname("/foo") == "/"); + assert(dirname("/") == "/"); + assert(dirname("foo/bar") == "foo"); + assert(dirname("foo") == "foo"); }; // Returns the final component of a given path. For a path to a file name, this // returns the file name. For a path to a directory, this returns the directory // name. The return value is borrowed from the input, use [dup] to extend its // lifetime. -export fn basename(path: path::path) path::path = { - let b = pathbytes(normalize(path)); +export fn basename(path: str) str = { + let b = strings::to_utf8(path); let i = match (bytes::rindex(b, PATHSEP)) { void => return path, z: size => if (z + 1 < len(b)) z + 1z else 0z, }; - return - if (path is str) strings::from_utf8_unsafe(b[i..]) - else b[i..]; + return strings::from_utf8_unsafe(b[i..]); }; @test fn basename() void = { - let p: path = "/foo/bar"; - assert(basename(p) as str == "bar"); - let p: path = "/foo"; - assert(basename(p) as str == "foo"); - let p: path = "/"; - assert(basename(p) as str == "/"); - let p: path = "foo/bar"; - assert(basename(p) as str == "bar"); - let p: path = "foo"; - assert(basename(p) as str == "foo"); + assert(basename("/foo/bar") == "bar"); + assert(basename("/foo") == "foo"); + assert(basename("/") == "/"); + assert(basename("foo/bar") == "bar"); + assert(basename("foo") == "foo"); }; -// Returns the file extension for a path. This presumes that the file extension -// is a valid UTF-8 string, if not, it will return an empty string. The return -// value is borrowed from the input, see [strings::dup] to extend its lifetime. +// Returns the file extension for a path. The return value is borrowed from the +// input, see [strings::dup] to extend its lifetime. // // The return value includes the '.' character. // // extension("foo/example") => "" // extension("foo/example.txt") => ".txt" // extension("foo/example.tar.gz") => ".tar.gz" -export fn extension(p: path::path) str = { - let b = pathbytes(p); +export fn extension(p: str) str = { + let b = strings::to_utf8(p); if (len(b) == 0 || b[len(b) - 1] == PATHSEP) { return ""; }; - let b = pathbytes(basename(p)); + let b = strings::to_utf8(basename(p)); let i = match (bytes::index(b, '.': u32: u8)) { void => return "", z: size => z, }; let e = b[i..]; - return match (strings::try_from_utf8(e)) { - utf8::invalid => "", - s: str => s, - }; + return strings::from_utf8_unsafe(e); }; @test fn extension() void = { diff --git a/path/norm.ha b/path/norm.ha @@ -1,6 +0,0 @@ -// Normalizes a path, merging successive path separators, and resolving . and .. -// components. Modifies the path in-place and returns the same object. -export fn normalize(p: path::path) path::path = { - // TODO - return p; -}; diff --git a/path/util.ha b/path/util.ha @@ -1,38 +1,8 @@ -use bytes; use strings; -// A path or path component. -export type path = (str | []u8); - -fn pathbytes(path: path) []u8 = match (path) { - s: str => strings::to_utf8(s), - b: []u8 => b, -}; - -// Frees a path. -export fn path_free(p: path) void = match (p) { - s: str => free(s), - b: []u8 => free(b), -}; - -// Duplicates a path. -export fn dup(p: path) path = match (p) { - s: str => strings::dup(s), - b: []u8 => { - // TODO: alloc from another slice - let new: []u8 = alloc([], len(b)); - append(new, ...b); - new; - }, -}; - -// Returns true if two paths are equal. -export fn equal(a: path, b: path) bool = - bytes::equal(pathbytes(a), pathbytes(b)); - // Returns true if a path is an absolute path. -export fn abs(path: path::path) bool = { - let b = pathbytes(path); +export fn abs(path: str) bool = { + let b = strings::to_utf8(path); if (len(b) == 0) { return false; }; diff --git a/temp/+linux.ha b/temp/+linux.ha @@ -5,7 +5,7 @@ use io; use os; use path; -fn get_tmpdir() path::path = os::tryenv("TMPDIR", "/tmp"); +fn get_tmpdir() str = os::tryenv("TMPDIR", "/tmp"); // Creates an unnamed temporary file. The file may or may not have a name; not // all systems support the creation of temporary inodes which are not linked to @@ -53,8 +53,8 @@ export fn named( // but not that it will be removed automatically; the caller must remove it when // they're done using it via [os::rmdir] or [os::rmdirall]. // -// The caller must free the return value with [path::path_free]. -export fn dir() path::path = { +// The caller must free the return value. +export fn dir() str = { let buf: [8]u8 = [0...]; random::buffer(buf[..]); let name = hex::encode(buf);