hare: fs,os: add copy() v2 SUPERSEDED

Autumn!: 1
 fs,os: add copy()

 2 files changed, 28 insertions(+), 0 deletions(-)
#988948 alpine.yml success
#988949 freebsd.yml success
having put more thought into this, i'm not sure if this is actually 
buggy behavior, since the permissions are not actually used until after 
the file is successfully opened, and they're just copied across to the 
new file. i don't really see where the potential unexpected behavior 
lies here. ~illiliti's point about the permissions being filtered 
through umask is relevant though and can be fixed i think.

alternatively, if there is in fact a bug here, fixing this will require 
an uncomfortably large refactor of fs:: and io::, since fstat would 
probably want to be io::stat since it deals with file descriptors, but 
it relies on fs::filestat, which can't be used by io since fs relies on 
io already, so it might need to move to io::filestat which feels wrong. 
there's a big tangled can of worms here that i don't want to get into tbqh.

Okay, in scope. If you can find a TOCTOU approach that'd be nice but if
not then no worries, make sure to document the edge case.
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/hare-dev/patches/41097/mbox | git am -3
Learn more about email & git

[PATCH hare v2] fs,os: add copy() Export this patch

Signed-off-by: Autumn! <autumnull@posteo.net>
accidentally did plagiarism badly
 fs/fs.ha | 23 +++++++++++++++++++++++
 os/os.ha |  5 +++++
 2 files changed, 28 insertions(+)

diff --git a/fs/fs.ha b/fs/fs.ha
index af630261..c7335bb7 100644
--- a/fs/fs.ha
+++ b/fs/fs.ha
@@ -102,6 +102,29 @@ export fn rename(fs: *fs, oldpath: str, newpath: str) (void | error) = {

// Copy a file from oldpath to newpath. Preserves the file permissions.
export fn copy(fs: *fs, oldpath: str, newpath: str) (void | error) = {
	// TODO: copy non-regular files
	let st = stat(fs, oldpath)?;
	assert(isfile(st.mode), "TODO: copy non-regular files");
	let old = open(fs, oldpath)?;
	let new = match (create(fs, newpath, st.mode)) {
	case let h: io::handle =>
		yield h;
	case let err: error =>
		io::close(old): void;
		return err;
	match (io::copy(new, old)) {
	case let err: io::error =>
		io::close(new): void;
		io::close(old): void;
		remove(fs, newpath)?;
		return err;
	case size => void;

// Moves a file. This will use [[rename]] if possible, and will fall back to
// copy and remove if necessary.
export fn move(fs: *fs, oldpath: str, newpath: str) (void | error) = {
diff --git a/os/os.ha b/os/os.ha
index 69e6ae77..4cb76275 100644
--- a/os/os.ha
+++ b/os/os.ha
@@ -19,6 +19,11 @@ export fn remove(path: str) (void | fs::error) = fs::remove(cwd, path);
export fn rename(oldpath: str, newpath: str) (void | fs::error) =
	fs::rename(cwd, oldpath, newpath);

// Copies a file from oldpath to newpath. Preserves the permissions.
export fn copy(oldpath: str, newpath: str) (void | fs::error) =
	fs::copy(cwd, oldpath, newpath);

// Moves a file. This will use [[rename]] if possible, and will fall back to
// copy and remove if necessary.
export fn move(oldpath: str, newpath: str) (void | fs::error) =
hare/patches: SUCCESS in 1m42s

[fs,os: add copy()][0] v2 from [Autumn!][1]

[0]: https://lists.sr.ht/~sircmpwn/hare-dev/patches/41097
[1]: mailto:autumnull@posteo.net

✓ #988948 SUCCESS hare/patches/alpine.yml  https://builds.sr.ht/~sircmpwn/job/988948
✓ #988949 SUCCESS hare/patches/freebsd.yml https://builds.sr.ht/~sircmpwn/job/988949
TOCTOU. Also maybe out of scope?