diff --git a/rt/src/runtime/fs.rs b/rt/src/runtime/fs.rs index 69f194121..873378cde 100644 --- a/rt/src/runtime/fs.rs +++ b/rt/src/runtime/fs.rs @@ -1,33 +1,8 @@ use crate::mem::String as InkoString; -use crate::process::ProcessPointer; use crate::result::Result as InkoResult; use crate::state::State; -use std::fs::{self}; use std::path::PathBuf; -#[no_mangle] -pub unsafe extern "system" fn inko_file_copy( - process: ProcessPointer, - from: *const InkoString, - to: *const InkoString, -) -> InkoResult { - process - .blocking(|| fs::copy(InkoString::read(from), InkoString::read(to))) - .map(|size| InkoResult::ok(size as _)) - .unwrap_or_else(InkoResult::io_error) -} - -#[no_mangle] -pub unsafe extern "system" fn inko_file_remove( - process: ProcessPointer, - path: *const InkoString, -) -> InkoResult { - process - .blocking(|| fs::remove_file(InkoString::read(path))) - .map(|_| InkoResult::none()) - .unwrap_or_else(InkoResult::io_error) -} - #[no_mangle] pub unsafe extern "system" fn inko_path_expand( state: *const State, @@ -43,47 +18,3 @@ pub unsafe extern "system" fn inko_path_expand( }) .unwrap_or_else(InkoResult::io_error) } - -#[no_mangle] -pub unsafe extern "system" fn inko_directory_create( - process: ProcessPointer, - path: *const InkoString, -) -> InkoResult { - process - .blocking(|| fs::create_dir(InkoString::read(path))) - .map(|_| InkoResult::none()) - .unwrap_or_else(InkoResult::io_error) -} - -#[no_mangle] -pub unsafe extern "system" fn inko_directory_create_recursive( - process: ProcessPointer, - path: *const InkoString, -) -> InkoResult { - process - .blocking(|| fs::create_dir_all(InkoString::read(path))) - .map(|_| InkoResult::none()) - .unwrap_or_else(InkoResult::io_error) -} - -#[no_mangle] -pub unsafe extern "system" fn inko_directory_remove( - process: ProcessPointer, - path: *const InkoString, -) -> InkoResult { - process - .blocking(|| fs::remove_dir(InkoString::read(path))) - .map(|_| InkoResult::none()) - .unwrap_or_else(InkoResult::io_error) -} - -#[no_mangle] -pub unsafe extern "system" fn inko_directory_remove_recursive( - process: ProcessPointer, - path: *const InkoString, -) -> InkoResult { - process - .blocking(|| fs::remove_dir_all(InkoString::read(path))) - .map(|_| InkoResult::none()) - .unwrap_or_else(InkoResult::io_error) -} diff --git a/std/fixtures/hello.txt b/std/fixtures/hello.txt new file mode 100644 index 000000000..ce0136250 --- /dev/null +++ b/std/fixtures/hello.txt @@ -0,0 +1 @@ +hello diff --git a/std/src/std/fs.inko b/std/src/std/fs.inko index 3922c3648..23f863808 100644 --- a/std/src/std/fs.inko +++ b/std/src/std/fs.inko @@ -117,6 +117,9 @@ class pub Metadata { # The type of the file. let pub @type: FileType + # The ownership/mode of the file. + let @mode: Int + # The size of the file in bytes. let pub @size: Int diff --git a/std/src/std/fs/file.inko b/std/src/std/fs/file.inko index 29c935797..979d744db 100644 --- a/std/src/std/fs/file.inko +++ b/std/src/std/fs/file.inko @@ -45,7 +45,13 @@ class pub ReadOnlyFile { # ``` fn pub static new(path: Path) -> Result[ReadOnlyFile, Error] { match - sys.open_file(path.to_string, read: true, write: false, append: false) + sys.open_file( + path.to_string, + read: true, + write: false, + append: false, + truncate: false, + ) { case Ok(fd) -> Result.Ok(ReadOnlyFile(fd: fd, path: path)) case Error(e) -> Result.Error(e) @@ -113,7 +119,13 @@ class pub WriteOnlyFile { # ``` fn pub static new(path: Path) -> Result[WriteOnlyFile, Error] { match - sys.open_file(path.to_string, read: false, write: true, append: false) + sys.open_file( + path.to_string, + read: false, + write: true, + append: false, + truncate: true, + ) { case Ok(fd) -> Result.Ok(WriteOnlyFile(fd: fd, path: path)) case Error(e) -> Result.Error(e) @@ -131,7 +143,13 @@ class pub WriteOnlyFile { # ``` fn pub static append(path: Path) -> Result[WriteOnlyFile, Error] { match - sys.open_file(path.to_string, read: false, write: true, append: true) + sys.open_file( + path.to_string, + read: false, + write: true, + append: true, + truncate: false, + ) { case Ok(fd) -> Result.Ok(WriteOnlyFile(fd: fd, path: path)) case Error(e) -> Result.Error(e) @@ -213,7 +231,13 @@ class pub ReadWriteFile { # ``` fn pub static new(path: Path) -> Result[ReadWriteFile, Error] { match - sys.open_file(path.to_string, read: true, write: true, append: false) + sys.open_file( + path.to_string, + read: true, + write: true, + append: false, + truncate: false, + ) { case Ok(fd) -> Result.Ok(ReadWriteFile(fd: fd, path: path)) case Error(e) -> Result.Error(e) @@ -230,7 +254,15 @@ class pub ReadWriteFile { # ReadWriteFile.append('/dev/null'.to_path) # ``` fn pub static append(path: Path) -> Result[ReadWriteFile, Error] { - match sys.open_file(path.to_string, read: true, write: true, append: true) { + match + sys.open_file( + path.to_string, + read: true, + write: true, + append: true, + truncate: false, + ) + { case Ok(fd) -> Result.Ok(ReadWriteFile(fd: fd, path: path)) case Error(e) -> Result.Error(e) } diff --git a/std/src/std/fs/path.inko b/std/src/std/fs/path.inko index 28f0ab3bb..c87ecb2c8 100644 --- a/std/src/std/fs/path.inko +++ b/std/src/std/fs/path.inko @@ -16,34 +16,6 @@ class extern AnyResult { let @value: UInt64 } -fn extern inko_file_remove(process: Pointer[UInt8], path: String) -> AnyResult - -fn extern inko_file_copy( - process: Pointer[UInt8], - from: String, - to: String, -) -> AnyResult - -fn extern inko_directory_remove( - process: Pointer[UInt8], - path: String, -) -> AnyResult - -fn extern inko_directory_create( - process: Pointer[UInt8], - path: String, -) -> AnyResult - -fn extern inko_directory_create_recursive( - process: Pointer[UInt8], - path: String, -) -> AnyResult - -fn extern inko_directory_remove_recursive( - process: Pointer[UInt8], - path: String, -) -> AnyResult - fn extern inko_path_expand(state: Pointer[UInt8], path: String) -> AnyResult # The byte used to represent a single dot/period. @@ -649,7 +621,10 @@ class pub Path { # Removes the file `self` points to. # - # If `self` points to a directory, an error is returned. + # # Errors + # + # This method returns an `Error` if the file `self` points to can't be removed + # (e.g. it doesn't exist) or isn't a file. # # # Examples # @@ -660,21 +635,19 @@ class pub Path { # let path = Path.new('/tmp/test.txt') # let handle = WriteOnlyFile.new(path).get # - # handle.write_string('hello').get - # path.remove_file.get + # handle.write_string('hello') # => Result.Ok(nil) + # path.remove_file # => Result.Ok(nil) # ``` fn pub remove_file -> Result[Nil, Error] { - match inko_file_remove(_INKO.process, @path) { - case { @tag = 1, @value = _ } -> Result.Ok(nil) - case { @tag = _, @value = e } -> { - Result.Error(Error.from_os_error(e as Int)) - } - } + sys.remove_file(@path) } # Removes the directory `self` points to. # - # If `self` points to a file, an error is returned. + # # Errors + # + # This method returns an error if `self` points to a file or if the directory + # can't be removed (e.g. the user lacks the necessary permissions). # # # Examples # @@ -683,26 +656,23 @@ class pub Path { # # let path = Path.new('/tmp/foo') # - # path.create_directory.get - # path.remove_directory.get + # path.create_directory # => Result.Ok(nil) + # path.remove_directory # => Result.Ok(nil) # ``` fn pub remove_directory -> Result[Nil, Error] { - match inko_directory_remove(_INKO.process, @path) { - case { @tag = 1, @value = _ } -> Result.Ok(nil) - case { @tag = _, @value = e } -> { - Result.Error(Error.from_os_error(e as Int)) - } - } + sys.remove_directory(@path) } # Removes the directory and its contents `self` points to. # - # # Errors + # When encountering symbolic links, the link itself is removed instead of the + # file it points to. # - # This method returns an `Error` if any of the following conditions are met: + # # Errors # - # 1. The user lacks the necessary permissions to remove the directory. - # 2. The directory does not exist. + # This method returns an enty if any of the directories or the contents can't + # be removed, such as when the user lacks the necessary permissions, or if + # `self` points to something other than a directory. # # # Examples # @@ -711,16 +681,48 @@ class pub Path { # ```inko # import std.fs.path (Path) # - # Path.new('/tmp/foo/bar').create_directory_all.get - # Path.new('/tmp/foo').remove_directory_all.get + # Path.new('/tmp/foo/bar').create_directory_all # => Result.Ok(nil) + # Path.new('/tmp/foo').remove_directory_all # => Result.Ok(nil) # ``` fn pub remove_directory_all -> Result[Nil, Error] { - match inko_directory_remove_recursive(_INKO.process, @path) { - case { @tag = 1, @value = _ } -> Result.Ok(nil) - case { @tag = _, @value = e } -> { - Result.Error(Error.from_os_error(e as Int)) + let stack = [@path] + let dirs = [@path] + + # First we remove all the files and gather the directories that need to be + # removed. + loop { + let dir = match stack.pop { + case Some(v) -> v + case _ -> break + } + + let iter = try sys.ReadDirectory.new(dir) + + try iter.try_each(fn (entry) { + match entry { + case Ok((name, Directory)) -> { + let path = join_strings(dir, name) + + stack.push(path) + dirs.push(path) + } + case Ok((name, _)) -> try sys.remove_file(join_strings(dir, name)) + case Error(e) -> throw e + } + + Result.Ok(nil) + }) + } + + # Now we can remove the directories in a depth-first order. + loop { + match dirs.pop { + case Some(v) -> try sys.remove_directory(v) + case _ -> break } } + + Result.Ok(nil) } # Creates a new empty directory at the path `self` points to. @@ -737,46 +739,59 @@ class pub Path { # ```inko # import std.fs.path (Path) # - # Path.new('/tmp/test').create_directory.get + # Path.new('/tmp/test').create_directory # => Result.Ok(nil) # ``` fn pub create_directory -> Result[Nil, Error] { - match inko_directory_create(_INKO.process, @path) { - case { @tag = 1, @value = _ } -> Result.Ok(nil) - case { @tag = _, @value = e } -> { - Result.Error(Error.from_os_error(e as Int)) - } - } + sys.create_directory(@path) } # Creates a new empty directory at the path `self` points to, while also # creating any intermediate directories. # - # # Errors + # Unlike `Path.create_directory`, this method _doesn't_ return an `Error` if + # any of the directories already exist. # - # This method returns an `Error` if any of the following conditions are met: + # # Errors # - # 1. The user lacks the necessary permissions to create the directory. + # This method returns an `Error` if any of the directories can't be created, + # such as when the user doesn't have the required permissions to do so. # # # Examples # # ```inko # import std.fs.path (Path) # - # Path.new('/tmp/foo/bar/test').create_directory_all.get + # Path.new('/tmp/foo/bar/test').create_directory_all # => Result.Ok(nil) # ``` fn pub create_directory_all -> Result[Nil, Error] { - match inko_directory_create_recursive(_INKO.process, @path) { - case { @tag = 1, @value = _ } -> Result.Ok(nil) - case { @tag = _, @value = e } -> { - Result.Error(Error.from_os_error(e as Int)) - } + # A common case is when all leading directories already exist, in which case + # we can avoid the more expensive loop to create the intermediate + # directories. + match create_directory { + case Ok(_) or Error(AlreadyExists) -> return Result.Ok(nil) + case Error(NotFound) -> {} + case Error(e) -> throw e } + + try components.try_reduce('', fn (leading, cur) { + let path = join_strings(leading, cur) + + match sys.create_directory(path) { + case Ok(_) or Error(AlreadyExists) -> Result.Ok(path) + case Error(e) -> Result.Error(e) + } + }) + + Result.Ok(nil) } # Copies the file `self` points to the file `to` points to, returning the - # number of copied bytes. + # number of bytes copied. + # + # # Errors # - # If `self` or `to` points to a directory, an error is returned. + # This method returns an `Error` if the file couldn't be copied, such as when + # the source file doesn't exist or the user lacks the necessary permissions. # # # Examples # @@ -791,12 +806,7 @@ class pub Path { # path.copy(to: '/tmp/test2.txt').get # ``` fn pub copy[T: ToString](to: ref T) -> Result[Int, Error] { - match inko_file_copy(_INKO.process, @path, to.to_string) { - case { @tag = 0, @value = v } -> Result.Ok(v as Int) - case { @tag = _, @value = e } -> { - Result.Error(Error.from_os_error(e as Int)) - } - } + sys.copy_file(@path, to.to_string) } # Returns an iterator over the components in `self`. diff --git a/std/src/std/io.inko b/std/src/std/io.inko index b6853cfd8..959383c04 100644 --- a/std/src/std/io.inko +++ b/std/src/std/io.inko @@ -213,6 +213,10 @@ class pub enum Error { # socket that was closed without sending the `close_notify` message. case EndOfInput + # A file couldn't be renamed, linked or copied because the operation takes + # places across different devices. + case CrossDeviceLink + # An error not covered by the other constructor. # # The wrapped `Int` is the raw error code. @@ -262,6 +266,7 @@ class pub enum Error { case libc.EHOSTUNREACH -> Error.HostUnreachable case libc.EINPROGRESS -> Error.InProgress case libc.EFAULT -> Error.BadAddress + case libc.EXDEV -> Error.CrossDeviceLink case INVALID_DATA -> Error.InvalidData case UNEXPECTED_EOF -> Error.EndOfInput case val -> Error.Other(val) @@ -308,6 +313,9 @@ impl ToString for Error { case WouldBlock -> 'the operation would block' case BadAddress -> 'a memory address is in an invalid range' case InvalidData -> "the data provided isn't valid for the operation" + case CrossDeviceLink -> { + "the operation failed because it can't be performed across devices" + } case EndOfInput -> { 'the end of the input stream is reached, but more input is required' } @@ -351,6 +359,7 @@ impl Format for Error { case BadAddress -> 'BadAddress' case InvalidData -> 'InvalidData' case EndOfInput -> 'EndOfInput' + case CrossDeviceLink -> 'CrossDeviceLink' case Other(code) -> { formatter.tuple('Other').field(code).finish return @@ -396,6 +405,7 @@ impl Equal[ref Error] for Error { case (Other(a), Other(b)) -> a == b case (InvalidData, InvalidData) -> true case (EndOfInput, EndOfInput) -> true + case (CrossDeviceLink, CrossDeviceLink) -> true case _ -> false } } diff --git a/std/src/std/libc.inko b/std/src/std/libc.inko index 147a41a9d..284330bde 100644 --- a/std/src/std/libc.inko +++ b/std/src/std/libc.inko @@ -50,6 +50,7 @@ let EROFS = sys.EROFS let ESPIPE = sys.ESPIPE let ETIME = sys.ETIME let ETIMEDOUT = sys.ETIMEDOUT +let EXDEV = sys.EXDEV let IPPROTO_IP = sys.IPPROTO_IP let IPPROTO_IPV6 = sys.IPPROTO_IPV6 let IPPROTO_TCP = sys.IPPROTO_TCP @@ -84,6 +85,14 @@ fn readdir(path: Pointer[UInt8]) -> Pointer[sys.Dirent] { fn extern closedir(stream: Pointer[UInt8]) -> Int32 +fn extern mkdir(path: Pointer[UInt8], mode: UInt32) -> Int32 + +fn extern rmdir(path: Pointer[UInt8]) -> Int32 + +fn extern unlink(path: Pointer[UInt8]) -> Int32 + +fn extern fchmod(fd: Int32, mode: UInt16) -> Int32 + # Opens the file at `path` with a set of flags and an optional mode. See # `open(2)` for more details. # diff --git a/std/src/std/libc/freebsd.inko b/std/src/std/libc/freebsd.inko index fcb404223..f8f01c809 100644 --- a/std/src/std/libc/freebsd.inko +++ b/std/src/std/libc/freebsd.inko @@ -17,6 +17,7 @@ let EHOSTUNREACH = 65 let EINPROGRESS = 36 let EINTR = 4 let EINVAL = 22 +let EIO = 5 let EISCONN = 56 let EISDIR = 21 let ENAMETOOLONG = 63 @@ -25,14 +26,17 @@ let ENETUNREACH = 51 let ENOENT = 2 let ENOMEM = 12 let ENOSPC = 28 +let ENOSYS = 78 let ENOTCONN = 57 let ENOTDIR = 20 let ENOTEMPTY = 66 +let EOPNOTSUPP = 45 let EPERM = 1 let EPIPE = 32 let EROFS = 30 let ESPIPE = 29 let ETIMEDOUT = 60 +let EXDEV = 18 let IPPROTO_IP = 0 let IPPROTO_IPV6 = 41 let IPPROTO_TCP = 6 @@ -120,6 +124,15 @@ fn extern readdir(stream: Pointer[UInt8]) -> Pointer[Dirent] fn extern fsync(fd: Int32) -> Int32 +fn extern copy_file_range( + in: Int32, + off_in: Pointer[Int64], + out: Int32, + off_out: Pointer[Int64], + len: UInt64, + flags: UInt32, +) -> Int64 + fn flush(fd: Int32) -> Int32 { fsync(fd) } diff --git a/std/src/std/libc/linux.inko b/std/src/std/libc/linux.inko index d7fb693db..3d7e49066 100644 --- a/std/src/std/libc/linux.inko +++ b/std/src/std/libc/linux.inko @@ -22,6 +22,7 @@ let EHOSTUNREACH = 113 let EINPROGRESS = 115 let EINTR = 4 let EINVAL = 22 +let EIO = 5 let EISCONN = 106 let EISDIR = 21 let ENAMETOOLONG = 36 @@ -30,15 +31,18 @@ let ENETUNREACH = 101 let ENOENT = 2 let ENOMEM = 12 let ENOSPC = 28 +let ENOSYS = 38 let ENOTCONN = 107 let ENOTDIR = 20 let ENOTEMPTY = 39 +let EOPNOTSUPP = 95 let EPERM = 1 let EPIPE = 32 let EROFS = 30 let ESPIPE = 29 let ETIME = 62 let ETIMEDOUT = 110 +let EXDEV = 18 let IPPROTO_IP = 0 let IPPROTO_IPV6 = 41 let IPPROTO_TCP = 6 @@ -71,8 +75,8 @@ let TCP_NODELAY = 1 let AT_EMPTY_PATH = 0x1000 let AT_FDCWD = -0x64 -let STATX_BASIC_STATS = 0x07FF -let STATX_BTIME = 0x0800 +let STATX_BASIC_STATS = 0x7FF +let STATX_BTIME = 0x800 class extern Dirent { let @d_ino: UInt64 @@ -133,6 +137,22 @@ fn extern readdir(stream: Pointer[UInt8]) -> Pointer[Dirent] fn extern fsync(fd: Int32) -> Int32 +fn extern sendfile( + out: Int32, + in: Int32, + offset: Pointer[Int64], + count: UInt64, +) -> Int64 + +fn extern copy_file_range( + in: Int32, + off_in: Pointer[Int64], + out: Int32, + off_out: Pointer[Int64], + len: UInt64, + flags: UInt32, +) -> Int64 + fn flush(fd: Int32) -> Int32 { fsync(fd) } diff --git a/std/src/std/libc/mac.inko b/std/src/std/libc/mac.inko index 90583eea1..12c3fdffa 100644 --- a/std/src/std/libc/mac.inko +++ b/std/src/std/libc/mac.inko @@ -37,6 +37,7 @@ let EROFS = 30 let ESPIPE = 29 let ETIME = 101 let ETIMEDOUT = 60 +let EXDEV = 18 let F_BARRIERFSYNC = 85 let F_FULLFSYNC = 51 let IPPROTO_IP = 0 @@ -109,6 +110,22 @@ fn extern fsync(fd: Int32) -> Int32 fn extern fcntl(fd: Int32, cmd: Int32, ...) -> Int32 +fn extern fclonefileat( + from: Int32, + dir: Int32, + to: Pointer[UInt8], + flags: Int32, +) -> Int32 + +fn extern copyfile_state_alloc -> Pointer[UInt8] + +fn extern fcopyfile( + from: Int32, + to: Int32, + state: Pointer[UInt8], + flags: UInt32, +) -> Int32 + fn fstat(fd: Int32, buf: Pointer[StatBuf]) -> Int32 { sys.fstat(fd, buf) } diff --git a/std/src/std/sys/freebsd/fs.inko b/std/src/std/sys/freebsd/fs.inko index b10219aac..d05156592 100644 --- a/std/src/std/sys/freebsd/fs.inko +++ b/std/src/std/sys/freebsd/fs.inko @@ -2,6 +2,9 @@ import std.fs (FileType, Metadata, Time) import std.io (Error, start_blocking, stop_blocking) import std.libc.freebsd (self as libc) +# The maximum number of bytes we can copy in a single system call. +let MAX_COPY = 0x7fffffffffffffff + fn stat_time(time: Pointer[libc.Timespec]) -> Time { Time(secs: time.tv_sec as Int, nanos: time.tv_nsec as Int) } @@ -14,6 +17,7 @@ fn stat_to_metadata(buf: Pointer[libc.StatBuf]) -> Metadata { case libc.S_IFLNK -> FileType.SymbolicLink case _ -> FileType.Other }, + mode: buf.st_mode as Int, size: buf.st_size as Int, created_at: Option.Some(stat_time(buf.st_birthtim)), modified_at: stat_time(buf.st_mtim), @@ -104,3 +108,47 @@ fn path_metadata(path: String) -> Result[Metadata, Error] { Result.Ok(stat_to_metadata(mut buf)) } + +fn copy_file_kernel(from: Int32, to: Int32) -> Result[Int, Error] { + let mut copied = 0 + + loop { + start_blocking + + let res = libc.copy_file_range( + in: from, + off_in: 0x0 as Pointer[Int64], + out: to, + off_out: 0x0 as Pointer[Int64], + len: MAX_COPY as UInt64, + flags: 0 as UInt32, + ) + as Int + let err = stop_blocking + + match res { + case 0 -> break + case -1 -> { + match err { + # Starting with FreeBSD 14, copy_file_range() shouldn't produce EXDEV, + # but it's not clear what FreeBSD 13 does, so we handle it just in + # case. Some of the wording in https://reviews.freebsd.org/D42603 + # suggests that at least on ZFS it should never be produced. + case + libc.EXDEV + or libc.EINVAL + or libc.EIO + or libc.ENOSYS + or libc.EOPNOTSUPP + -> { + return Result.Ok(-1) + } + case _ -> throw Error.from_os_error(err) + } + } + case n -> copied += n + } + } + + if copied == 0 { Result.Ok(-1) } else { Result.Ok(copied) } +} diff --git a/std/src/std/sys/linux/fs.inko b/std/src/std/sys/linux/fs.inko index a826e9289..8b1e9943e 100644 --- a/std/src/std/sys/linux/fs.inko +++ b/std/src/std/sys/linux/fs.inko @@ -2,6 +2,9 @@ import std.fs (FileType, Metadata, Time) import std.io (Error, start_blocking, stop_blocking) import std.libc.linux (self as libc) +# The maximum number of bytes we can copy in a single system call. +let MAX_COPY = 0x7ffff000 + fn statx_time(time: Pointer[libc.StatxTimestamp]) -> Time { Time(secs: time.tv_sec as Int, nanos: time.tv_nsec as Int) } @@ -82,6 +85,7 @@ fn statx(fd: Int32, path: String) -> Result[Metadata, Error] { case libc.S_IFLNK -> FileType.SymbolicLink case _ -> FileType.Other }, + mode: buf.stx_mode as Int, size: buf.stx_size as Int, created_at: if buf.stx_mask as Int & libc.STATX_BTIME == 0 { Option.None @@ -101,3 +105,88 @@ fn file_metadata(fd: Int32) -> Result[Metadata, Error] { fn path_metadata(path: String) -> Result[Metadata, Error] { statx(libc.AT_FDCWD as Int32, path: path) } + +fn sendfile_copy(from: Int32, to: Int32) -> Result[Int, Int] { + let mut copied = 0 + let max = MAX_COPY as UInt64 + + loop { + start_blocking + + let res = libc.sendfile(to, from, 0x0 as Pointer[Int64], max) as Int + let err = stop_blocking + + match res { + case 0 -> break + case -1 -> throw err + case n -> copied += n + } + } + + Result.Ok(copied) +} + +fn copy_file_kernel(from: Int32, to: Int32) -> Result[Int, Error] { + let mut copied = 0 + let mut fallback = false + + loop { + start_blocking + + let res = libc.copy_file_range( + in: from, + off_in: 0x0 as Pointer[Int64], + out: to, + off_out: 0x0 as Pointer[Int64], + len: MAX_COPY as UInt64, + flags: 0 as UInt32, + ) + as Int + let err = stop_blocking + + match res { + case 0 -> break + case -1 -> { + match err { + # copy_file_range() doesn't support copying files across devices, and + # certain file systems might not support it. In some cases ENOSYS is + # produced instead of e.g. EOPNOTSUPP + # (https://github.com/libuv/libuv/issues/3069). + # + # EINVAL is produced if the source and destination are the same. + case + libc.EXDEV + or libc.EINVAL + or libc.EIO + or libc.ENOSYS + or libc.EOPNOTSUPP + -> { + fallback = true + break + } + case _ -> throw Error.from_os_error(err) + } + } + case n -> copied += n + } + } + + # Certain versions of Linux return 0 when trying to copy from special file + # systems (https://github.com/golang/go/issues/44272). When encountering such + # a case we fall back to using sendfile. + # + # In case of an empty file this may result in a redundant system call, but due + # to the file being empty the cost of that should be minimal. + if fallback or copied == 0 { + match sendfile_copy(from, to) { + case Ok(n) -> Result.Ok(n) + case Error(libc.ENOSYS or libc.EINVAL) -> { + # sendfile() might fail when copying certain "files" such as /dev/stdin. + Result.Ok(-1) + } + case Error(e) -> throw Error.from_os_error(e) + } + } else { + Result.Ok(copied) + } +} diff --git a/std/src/std/sys/mac/fs.inko b/std/src/std/sys/mac/fs.inko index 4d6ce9128..77686f6fb 100644 --- a/std/src/std/sys/mac/fs.inko +++ b/std/src/std/sys/mac/fs.inko @@ -10,6 +10,7 @@ fn stat_to_metadata(buf: Pointer[libc.StatBuf]) -> Metadata { case libc.S_IFLNK -> FileType.SymbolicLink case _ -> FileType.Other }, + mode: buf.st_mode as Int, size: buf.st_size as Int, created_at: Option.Some( Time(secs: buf.st_birthtime as Int, nanos: buf.st_birthtime_nsec as Int), @@ -98,3 +99,8 @@ fn path_metadata(path: String) -> Result[Metadata, Error] { Result.Ok(stat_to_metadata(mut buf)) } + +fn copy_file_kernel(from: Int32, to: Int32) -> Result[Int, Error] { + # TODO: implement + Result.Error(Error.PermissionDenied) +} diff --git a/std/src/std/sys/unix/fs.inko b/std/src/std/sys/unix/fs.inko index d04161e33..d76b1945b 100644 --- a/std/src/std/sys/unix/fs.inko +++ b/std/src/std/sys/unix/fs.inko @@ -1,6 +1,6 @@ import std.drop (Drop) -import std.fs (FileType) -import std.fs (Metadata) +import std.fs (FileType, Metadata) +import std.fs.file (ReadOnlyFile, WriteOnlyFile) import std.io (Error, reset_os_error, start_blocking, stop_blocking) import std.iter (Iter) import std.libc @@ -9,13 +9,15 @@ import std.sys.linux.fs (self as sys) if linux import std.sys.mac.fs (self as sys) if mac let DOT = 46 -let MODE = 0x1B6 # 666 +let FILE_MODE = 0x1B6 # 666 +let DIR_MODE = 0x1FF # 777 fn open_file( path: String, read: Bool, write: Bool, append: Bool, + truncate: Bool, ) -> Result[Int32, Error] { let path_ptr = path.to_pointer let mut flags = libc.O_CLOEXEC @@ -29,9 +31,11 @@ fn open_file( if append { flags |= libc.O_APPEND } + if truncate { flags |= libc.O_TRUNC } + start_blocking - let file = libc.open(path_ptr, flags as Int32, MODE as Int32) + let file = libc.open(path_ptr, flags as Int32, FILE_MODE as Int32) let err = stop_blocking if file as Int >= 0 { @@ -111,6 +115,97 @@ fn path_metadata(path: String) -> Result[Metadata, Error] { sys.path_metadata(path) } +fn create_directory(path: String) -> Result[Nil, Error] { + start_blocking + + let res = libc.mkdir(path.to_pointer, DIR_MODE as UInt32) as Int + let err = stop_blocking + + if res == 0 { Result.Ok(nil) } else { Result.Error(Error.from_os_error(err)) } +} + +fn remove_directory(path: String) -> Result[Nil, Error] { + start_blocking + + let res = libc.rmdir(path.to_pointer) as Int + let err = stop_blocking + + if res == 0 { Result.Ok(nil) } else { Result.Error(Error.from_os_error(err)) } +} + +fn remove_file(path: String) -> Result[Nil, Error] { + start_blocking + + let res = libc.unlink(path.to_pointer) as Int + let err = stop_blocking + + if res == 0 { Result.Ok(nil) } else { Result.Error(Error.from_os_error(err)) } +} + +fn copy_file(from: String, to: String) -> Result[Int, Error] { + let from_file = try ReadOnlyFile.new(from.to_path) + let meta = try from_file.metadata + let to_file = try WriteOnlyFile.new(to.to_path) + let copied = match try sys.copy_file_kernel(from_file.fd, to_file.fd) { + case -1 -> try copy_file_fallback(from_file, to_file) + case n -> n + } + + start_blocking + + let res = libc.fchmod(to_file.fd, meta.mode as UInt16) as Int + let err = stop_blocking + + if res == 0 { + Result.Ok(copied) + } else { + Result.Error(Error.from_os_error(err)) + } +} + +fn copy_file_fallback( + from: mut ReadOnlyFile, + to: mut WriteOnlyFile, +) -> Result[Int, Error] { + # To provide a balance between performance and memory usage, we've chosen the + # arbitrary buffer size of 1 MiB. + let buffer = ByteArray.with_capacity(1024 * 1024) + let mut copied = 0 + + loop { + let mut pending = buffer.capacity + + match from.read(into: buffer, size: buffer.capacity) { + case Ok(0) if buffer.size > 0 -> { + # The file is smaller than the buffer capacity, so we need to flush + # the remaining data. + try to.write_bytes(buffer) + break + } + case Ok(0) -> break + case Ok(n) -> { + pending -= n + copied += n + + # In case we end up reading less than the requested amount we buffer up + # the data. This ensures that if we have many instances of smaller + # reads, we still keep the number of writes to a minimum. + if pending == 0 { + try to.write_bytes(buffer) + buffer.clear + pending = buffer.capacity + } + } + case Error(e) -> throw e + } + } + + # Make sure the changes are actually persisted. This may be overly expensive + # in some cases, but it's better to be safe than sorry. + try to.flush + Result.Ok(copied) +} + class ReadDirectory { let @stream: Pointer[UInt8] diff --git a/std/test/std/fs/test_file.inko b/std/test/std/fs/test_file.inko index a79735d5a..78d82e7c0 100644 --- a/std/test/std/fs/test_file.inko +++ b/std/test/std/fs/test_file.inko @@ -87,22 +87,44 @@ fn pub tests(t: mut Tests) { Result.Ok(nil) }) - t.test('WriteOnlyFile.new', fn (t) { + t.ok('WriteOnlyFile.new', fn (t) { let path = env.temporary_directory.join('inko-test-${t.id}') - t.true(WriteOnlyFile.new(path.clone).ok?) - t.true(WriteOnlyFile.new(path.clone).ok?) + { + let file = try WriteOnlyFile.new(path.clone) - path.remove_file.get + try file.write_string('hello') + try file.flush + + t.equal(read(path), 'hello') + } + + # Opening an existing file should truncate it. + try WriteOnlyFile.new(path.clone) + + t.equal(read(path), '') + path.remove_file }) - t.test('WriteOnlyFile.append', fn (t) { + t.ok('WriteOnlyFile.append', fn (t) { let path = env.temporary_directory.join('inko-test-${t.id}') - t.true(WriteOnlyFile.append(path.clone).ok?) - t.true(WriteOnlyFile.append(path.clone).ok?) + { + let file = try WriteOnlyFile.append(path.clone) - path.remove_file.get + try file.write_string('hello') + try file.flush + } + + { + let file = try WriteOnlyFile.append(path.clone) + + try file.write_string('world') + try file.flush + } + + t.equal(read(path), 'helloworld') + path.remove_file }) t.test('WriteOnlyFile.write_bytes', fn (t) { @@ -193,13 +215,29 @@ fn pub tests(t: mut Tests) { Result.Ok(nil) }) - t.test('ReadWriteFile.new', fn (t) { + t.ok('ReadWriteFile.new', fn (t) { let path = env.temporary_directory.join('inko-test-${t.id}') - t.true(ReadWriteFile.new(path.clone).ok?) - t.true(ReadWriteFile.new(path.clone).ok?) + { + let file = try ReadWriteFile.new(path.clone) + let buf = ByteArray.new - path.remove_file.get + try file.write_string('hello') + try file.flush + try file.seek(0) + try file.read_all(buf) + + t.equal(buf.into_string, 'hello') + } + + # Opening an existing file _shouldn't_ truncate it. + let file = try ReadWriteFile.new(path.clone) + let buf = ByteArray.new + + try file.read_all(buf) + + t.equal(buf.into_string, 'hello') + path.remove_file }) t.test('ReadWriteFile.append', fn (t) { diff --git a/std/test/std/fs/test_path.inko b/std/test/std/fs/test_path.inko index a38b6abfb..8b2df23af 100644 --- a/std/test/std/fs/test_path.inko +++ b/std/test/std/fs/test_path.inko @@ -1,11 +1,11 @@ -import helpers (hash, with_directory) +import helpers (compiler_path, hash, with_directory) import std.env import std.fmt (fmt) import std.fs (DirectoryEntry, FileType) import std.fs.file (self, ReadOnlyFile, WriteOnlyFile) import std.fs.path (self, Path) import std.io (Error) -import std.stdio (STDOUT) +import std.stdio (STDIN, STDOUT) import std.sys import std.test (Tests) import std.time (DateTime, Duration) @@ -295,7 +295,8 @@ fn pub tests(t: mut Tests) { }) }) - t.test('Path.copy', fn (t) { + t.test('Path.copy using regular files', fn (t) { + # Source and destination on the same file system. let path1 = env.temporary_directory.join('inko-test-${t.id}-1') let path2 = env.temporary_directory.join('inko-test-${t.id}-2') @@ -304,10 +305,35 @@ fn pub tests(t: mut Tests) { t.true(path1.copy(to: path2).ok?) t.equal(read(path2), 'test') + # Source and destination (almost certainly) not on the same file system. + t.true('fixtures/hello.txt'.to_path.copy(to: path2).ok?) + t.equal(read(path2), 'hello\n') + path1.remove_file.get path2.remove_file.get }) + t.fork( + 'Path.copy with a special file as the source', + child: fn { + let buf = ByteArray.new + + STDIN.new.read_all(buf).get + + let path = buf.into_string.to_path + + Path.new('/dev/stdin').copy(path).get + path.remove_file.get + STDOUT.new.write_string('ok') + }, + test: fn (t, proc) { + let path = env.temporary_directory.join('inko-test-${t.id}') + + proc.stdin(path.to_string) + t.equal(proc.spawn.stdout, 'ok') + }, + ) + t.test('Path.extension', fn (t) { t.equal(Path.new('').extension, Option.None) t.equal(Path.new(' ').extension, Option.None)