diff --git a/Cargo.lock b/Cargo.lock index c3b6f89fa..eb077dbba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,6 +146,22 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "crc32fast" version = "1.4.0" @@ -370,6 +386,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -476,6 +498,9 @@ dependencies = [ "libc", "rand", "rustix", + "rustls 0.23.7", + "rustls-native-certs", + "rustls-pemfile", "socket2", "unicode-segmentation", ] @@ -513,6 +538,43 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebbbdb961df0ad3f2652da8f3fdc4b36122f568f968f45ad3316f26c025c677b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64", + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.5.0" @@ -530,6 +592,38 @@ dependencies = [ "untrusted", ] +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "security-framework" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +dependencies = [ + "bitflags 2.5.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.22" @@ -686,7 +780,7 @@ dependencies = [ "base64", "log", "once_cell", - "rustls", + "rustls 0.22.4", "rustls-pki-types", "rustls-webpki", "url", diff --git a/rt/Cargo.toml b/rt/Cargo.toml index 9853cb5aa..eea5e0b6c 100644 --- a/rt/Cargo.toml +++ b/rt/Cargo.toml @@ -23,6 +23,19 @@ unicode-segmentation = "^1.10" backtrace = "^0.3" rustix = { version = "^0.38", features = ["fs", "mm", "param", "process", "net", "std", "time", "event"], default-features = false } +# The dependencies needed for TLS support. +# +# We use ring instead of the default aws-lc-sys because: +# +# 1. aws-lc-sys requires cmake to be installed when building on FreeBSD (and +# potentially other platforms), as aws-lc-sys only provides generated +# bindings for a limited set of platforms +# 2. aws-lc-sys increases compile times quite a bit +# 3. We don't care about FIPS compliance at the time of writing +rustls = { version = "^0.23", features = ["ring", "tls12", "std"], default-features = false } +rustls-native-certs = "^0.7" +rustls-pemfile = "^2.1" + [dependencies.socket2] version = "^0.5" features = ["all"] diff --git a/rt/src/network_poller.rs b/rt/src/network_poller.rs index e28c278d1..c5221edb2 100644 --- a/rt/src/network_poller.rs +++ b/rt/src/network_poller.rs @@ -28,6 +28,17 @@ pub(crate) type NetworkPoller = sys::Poller; pub(crate) enum Interest { Read, Write, + ReadWrite, +} + +impl Interest { + pub(crate) fn new(read: bool, write: bool) -> Interest { + match (read, write) { + (true, true) => Interest::ReadWrite, + (false, true) => Interest::Write, + _ => Interest::Read, + } + } } /// A thread that polls a poller and reschedules processes. diff --git a/rt/src/network_poller/epoll.rs b/rt/src/network_poller/epoll.rs index 6fecea8d8..c501a06b7 100644 --- a/rt/src/network_poller/epoll.rs +++ b/rt/src/network_poller/epoll.rs @@ -11,6 +11,7 @@ fn flags_for(interest: Interest) -> EventFlags { let flags = match interest { Interest::Read => EventFlags::IN, Interest::Write => EventFlags::OUT, + Interest::ReadWrite => EventFlags::IN | EventFlags::OUT, }; flags | EventFlags::ET | EventFlags::ONESHOT diff --git a/rt/src/network_poller/kqueue.rs b/rt/src/network_poller/kqueue.rs index 4d3790277..17e006dbf 100644 --- a/rt/src/network_poller/kqueue.rs +++ b/rt/src/network_poller/kqueue.rs @@ -40,18 +40,24 @@ impl Poller { source: impl AsFd, interest: Interest, ) { - let fd = source.as_fd().as_raw_fd(); - let (add, del) = match interest { - Interest::Read => (EventFilter::Read(fd), EventFilter::Write(fd)), - Interest::Write => (EventFilter::Write(fd), EventFilter::Read(fd)), - }; let id = process.identifier() as isize; + let fd = source.as_fd().as_raw_fd(); let flags = EventFlags::CLEAR | EventFlags::ONESHOT | EventFlags::RECEIPT; - let events = [ - Event::new(add, EventFlags::ADD | flags, id), - Event::new(del, EventFlags::DELETE, 0), - ]; + let events = match interest { + Interest::Read => [ + Event::new(EventFilter::Read(fd), EventFlags::ADD | flags, id), + Event::new(EventFilter::Write(fd), EventFlags::DELETE, 0), + ], + Interest::Write => [ + Event::new(EventFilter::Write(fd), EventFlags::ADD | flags, id), + Event::new(EventFilter::Read(fd), EventFlags::DELETE, 0), + ], + Interest::ReadWrite => [ + Event::new(EventFilter::Write(fd), EventFlags::ADD | flags, id), + Event::new(EventFilter::Read(fd), EventFlags::ADD | flags, id), + ], + }; self.apply(&events); } diff --git a/rt/src/result.rs b/rt/src/result.rs index d72a8b71d..2b9713fc4 100644 --- a/rt/src/result.rs +++ b/rt/src/result.rs @@ -60,7 +60,7 @@ impl Result { } pub(crate) fn io_error(error: io::Error) -> Result { - Self::error({ error_to_int(error) } as _) + Self::error(error_to_int(error) as _) } } diff --git a/rt/src/runtime.rs b/rt/src/runtime.rs index f027d53eb..fcaf9e44f 100644 --- a/rt/src/runtime.rs +++ b/rt/src/runtime.rs @@ -67,6 +67,12 @@ pub unsafe extern "system" fn inko_runtime_new( // does for us when compiling an executable. signal_sched::block_all(); + // Configure the TLS provider. This must be done once before we start the + // program. + rustls::crypto::ring::default_provider() + .install_default() + .expect("failed to set up the default TLS cryptography provider"); + Box::into_raw(Box::new(Runtime::new(&*counts, args))) } diff --git a/rt/src/runtime/env.rs b/rt/src/runtime/env.rs index dfb9dbd51..1f8bbb43f 100644 --- a/rt/src/runtime/env.rs +++ b/rt/src/runtime/env.rs @@ -40,26 +40,6 @@ pub unsafe extern "system" fn inko_env_size(state: *const State) -> i64 { (*state).environment.len() as _ } -#[no_mangle] -pub unsafe extern "system" fn inko_env_home_directory( - state: *const State, -) -> InkoResult { - let state = &*state; - - // Rather than performing all sorts of magical incantations to get the home - // directory, we're just going to require that HOME is set. - // - // If the home is explicitly set to an empty string we still ignore it, - // because there's no scenario in which Some("") is useful. - state - .environment - .get("HOME") - .filter(|&path| !path.is_empty()) - .cloned() - .map(|v| InkoResult::ok(InkoString::alloc(state.string_class, v) as _)) - .unwrap_or_else(InkoResult::none) -} - #[no_mangle] pub unsafe extern "system" fn inko_env_temp_directory( state: *const State, diff --git a/rt/src/runtime/socket.rs b/rt/src/runtime/socket.rs index 496b51200..d7a44ee24 100644 --- a/rt/src/runtime/socket.rs +++ b/rt/src/runtime/socket.rs @@ -4,10 +4,16 @@ use crate::network_poller::Interest; use crate::process::ProcessPointer; use crate::result::{error_to_int, Result}; use crate::scheduler::timeouts::Timeout; -use crate::socket::Socket; +use crate::socket::{read_from, Socket}; use crate::state::State; -use std::io::{self, Write}; +use rustls::pki_types::ServerName; +use rustls::{ClientConfig, ClientConnection, RootCertStore, Stream}; +use rustls_native_certs::load_native_certs; +use rustls_pemfile::certs; +use std::fs::File; +use std::io::{self, BufReader, Write}; use std::ptr::{drop_in_place, write}; +use std::sync::Arc; #[repr(C)] pub struct RawAddress { @@ -24,19 +30,13 @@ impl RawAddress { } } -fn blocking( +fn poll( state: &State, mut process: ProcessPointer, socket: &mut Socket, interest: Interest, deadline: i64, - mut func: impl FnMut(&mut Socket) -> io::Result, -) -> io::Result { - match func(socket) { - Err(err) if err.kind() == io::ErrorKind::WouldBlock => {} - val => return val, - } - +) -> io::Result<()> { let poll_id = unsafe { process.thread() }.network_poller; // We must keep the process' state lock open until everything is registered, @@ -72,7 +72,24 @@ fn blocking( return Err(io::Error::from(io::ErrorKind::TimedOut)); } - func(socket) + Ok(()) +} + +fn blocking( + state: &State, + process: ProcessPointer, + socket: &mut Socket, + interest: Interest, + deadline: i64, + mut func: impl FnMut(&mut Socket) -> io::Result, +) -> io::Result { + match func(socket) { + Err(err) if err.kind() == io::ErrorKind::WouldBlock => { + poll(state, process, socket, interest, deadline) + .and_then(|_| func(socket)) + } + val => val, + } } #[no_mangle] @@ -127,7 +144,7 @@ pub unsafe extern "system" fn inko_socket_read( let state = &*state; blocking(state, process, &mut *socket, Interest::Read, deadline, |sock| { - sock.read(&mut (*buffer).value, amount as usize) + read_from(sock, &mut (*buffer).value, amount as usize) }) .map(|size| Result::ok(size as _)) .unwrap_or_else(Result::io_error) @@ -349,3 +366,180 @@ pub unsafe extern "system" fn inko_socket_try_clone( pub unsafe extern "system" fn inko_socket_drop(socket: *mut Socket) { drop_in_place(socket); } + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_config_new() -> Result { + let mut store = RootCertStore::empty(); + let certs = match load_native_certs() { + Ok(v) => v, + Err(e) => return Result::io_error(e), + }; + + // It's possible that some certificates are bogus/not supported by rustls. + // In this case we ignore the certificate instead of crashing the entire + // program, as the end user likely doesn't need such a certificate nor is + // able to resolve the issue. + store.add_parsable_certificates(certs.into_iter()); + + let conf = Arc::new( + ClientConfig::builder() + .with_root_certificates(store) + .with_no_client_auth(), + ); + + Result::ok(Arc::into_raw(conf) as *mut _) +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_config_with_certificate( + path: *const InkoString, +) -> Result { + let mut store = RootCertStore::empty(); + let mut reader = match File::open(InkoString::read(path)) { + Ok(f) => BufReader::new(f), + Err(e) => return Result::io_error(e), + }; + + for res in certs(&mut reader) { + match res { + // We don't want to expose a bunch of error messages/cases for the + // different reasons for a certificate being invalid, as it's not + // clear users actually care about that. As such, at least for the + // time being we just use a single opaque error for invalid + // certificates. + Ok(cert) => { + if store.add(cert).is_err() { + return Result::none(); + } + } + Err(e) => return Result::io_error(e), + } + } + + let conf = Arc::new( + ClientConfig::builder() + .with_root_certificates(store) + .with_no_client_auth(), + ); + + Result::ok(Arc::into_raw(conf) as *mut _) +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_config_clone( + config: *const ClientConfig, +) -> *const ClientConfig { + Arc::increment_strong_count(config); + config +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_config_drop( + config: *const ClientConfig, +) { + drop(Arc::from_raw(config)); +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_connection_new( + config: *const ClientConfig, + server: *const InkoString, +) -> Result { + let name = match ServerName::try_from(InkoString::read(server)) { + Ok(v) => v, + Err(_) => return Result::error(0 as _), + }; + + Arc::increment_strong_count(config); + + // TODO: under what circumstance does this fail? + let con = match ClientConnection::new(Arc::from_raw(config), name) { + Ok(v) => v, + Err(_) => return Result::error(1 as _), + }; + + Result::ok_boxed(con) +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_connection_drop( + connection: *mut ClientConnection, +) { + drop(Box::from_raw(connection)); +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_socket_write( + state: *const State, + process: ProcessPointer, + socket: *mut Socket, + connection: *mut ClientConnection, + data: *mut u8, + size: i64, + deadline: i64, +) -> Result { + let state = &*state; + let slice = std::slice::from_raw_parts(data, size as _); + let mut stream = Stream::new(&mut *connection, &mut *socket); + + loop { + match stream.write(slice) { + Err(err) if err.kind() == io::ErrorKind::WouldBlock => { + let interest = Interest::new( + stream.conn.wants_read(), + stream.conn.wants_write(), + ); + + if let Err(e) = + poll(state, process, stream.sock, interest, deadline) + { + return Result::io_error(e); + } + } + val => { + return val + .map(|v| Result::ok(v as _)) + .unwrap_or_else(Result::io_error); + } + } + } +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_socket_read( + state: *const State, + process: ProcessPointer, + socket: *mut Socket, + connection: *mut ClientConnection, + buffer: *mut ByteArray, + amount: i64, + deadline: i64, +) -> Result { + let state = &*state; + let buf = &mut (*buffer).value; + let mut stream = Stream::new(&mut *connection, &mut *socket); + + loop { + match read_from(&mut stream, buf, amount as usize) { + Err(err) if err.kind() == io::ErrorKind::WouldBlock => { + let interest = Interest::new( + stream.conn.wants_read(), + stream.conn.wants_write(), + ); + + if let Err(e) = + poll(state, process, stream.sock, interest, deadline) + { + return Result::io_error(e); + } + + continue; + } + val => { + return val + .map(|v| Result::ok(v as _)) + .unwrap_or_else(Result::io_error); + } + }; + } +} diff --git a/rt/src/socket.rs b/rt/src/socket.rs index 5f881cf23..fc49d4a78 100644 --- a/rt/src/socket.rs +++ b/rt/src/socket.rs @@ -105,6 +105,29 @@ fn socket_type(kind: i64) -> io::Result { } } +pub(crate) fn read_from( + reader: &mut R, + into: &mut Vec, + amount: usize, +) -> io::Result { + if amount > 0 { + // We don't use take(), because that only terminates if: + // + // 1. We hit EOF, or + // 2. We have read the desired number of bytes + // + // For files this is fine, but for sockets EOF is not triggered + // until the socket is closed; which is almost always too late. + let slice = socket_output_slice(into, amount); + let read = reader.read(slice)?; + + update_buffer_length_and_capacity(into, read); + Ok(read) + } else { + Ok(reader.read_to_end(into)?) + } +} + /// A nonblocking socket that can be registered with a `NetworkPoller`. /// /// When changing the layout of this type, don't forget to also update its @@ -260,29 +283,6 @@ impl Socket { }) } - pub(crate) fn read( - &self, - buffer: &mut Vec, - amount: usize, - ) -> io::Result { - if amount > 0 { - // We don't use take(), because that only terminates if: - // - // 1. We hit EOF, or - // 2. We have read the desired number of bytes - // - // For files this is fine, but for sockets EOF is not triggered - // until the socket is closed; which is almost always too late. - let slice = socket_output_slice(buffer, amount); - let read = self.inner.recv(unsafe { transmute(slice) })?; - - update_buffer_length_and_capacity(buffer, read); - Ok(read) - } else { - Ok((&self.inner).read_to_end(buffer)?) - } - } - pub(crate) fn recv_from( &self, buffer: &mut Vec, @@ -358,7 +358,7 @@ impl io::Write for Socket { impl io::Read for Socket { fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.inner.recv(unsafe { transmute(buf) }) + self.inner.read(buf) } } diff --git a/std/src/std/env.inko b/std/src/std/env.inko index a6b85c756..e8d486783 100644 --- a/std/src/std/env.inko +++ b/std/src/std/env.inko @@ -37,8 +37,6 @@ fn extern inko_env_get(state: Pointer[UInt8], name: String) -> AnyResult fn extern inko_env_get_working_directory(state: Pointer[UInt8]) -> AnyResult -fn extern inko_env_home_directory(state: Pointer[UInt8]) -> AnyResult - fn extern inko_env_set_working_directory(path: String) -> AnyResult fn extern inko_env_temp_directory(state: Pointer[UInt8]) -> String @@ -119,10 +117,17 @@ fn pub variables -> Map[String, String] { # env.home_directory # => Option.Some('/home/alice') # ``` fn pub home_directory -> Option[Path] { - match inko_env_home_directory(_INKO.state) { - case { @tag = 0, @value = val } -> Option.Some(Path.new(val as String)) - case _ -> Option.None + # Rather than performing all sorts of magical incantations to get the home + # directory, we're just going to require that HOME is set. + # + # If the home is explicitly set to an empty string we still ignore it, because + # there's no scenario in which Some("") is useful. + let val = match inko_env_get(_INKO.state, 'HOME') { + case { @tag = 0, @value = val } -> val as String + case _ -> return Option.None } + + if val.size > 0 { Option.Some(Path.new(val)) } else { Option.None } } # Returns the path to the temporary directory. diff --git a/std/src/std/fs/path.inko b/std/src/std/fs/path.inko index 2ba9f9916..da38eca26 100644 --- a/std/src/std/fs/path.inko +++ b/std/src/std/fs/path.inko @@ -1,6 +1,7 @@ # Cross-platform path manipulation. import std.clone (Clone) import std.cmp (Equal) +import std.env (home_directory) import std.fmt (Format, Formatter) import std.fs (DirectoryEntry) import std.hash (Hash, Hasher) @@ -85,6 +86,13 @@ let pub SEPARATOR = '/' # The byte used to represent the path separator. let SEPARATOR_BYTE = 47 +# The character used to signal the user's home directory. +let HOME = '~' + +# The prefix of a path that indicates a path relative to the user's home +# directory. +let HOME_WITH_SEPARATOR = HOME + SEPARATOR + # Returns the number of bytes leading up to the last path separator. # # If no separator could be found, `-1` is returned. @@ -623,11 +631,20 @@ class pub Path { # Returns the canonical, absolute version of `self`. # + # # Resolving home directories + # + # If `self` is equal to `~`, this method returns the path to the user's home + # directory. If `self` starts with `~/`, this prefix is replaced with the path + # to the user's home directory (e.g. `~/foo` becomes `/var/home/alice/foo`). + # # # Errors # - # This method may return an `Error` for cases such as when `self` doesn't - # exist, or when a component that isn't the last component is _not_ a - # directory. + # This method may return an `Error` for cases such as: + # + # - `self` doesn't exist + # - a component that isn't the last component is _not_ a directory + # - `self` is equal to `~` or starts with `~/`, but the home directory can't + # be found (e.g. it doesn't exist) # # # Examples # @@ -635,9 +652,30 @@ class pub Path { # import std.fs.path (Path) # # Path.new('/foo/../bar').expand.get # => Path.new('/bar') + # Path.new('~').expand.get # => '/var/home/...' + # Path.new('~/').expand.get # => '/var/home/...' # ``` fn pub expand -> Result[Path, Error] { - match inko_path_expand(_INKO.state, @path) { + if @path == HOME { + return match home_directory { + case Some(v) -> Result.Ok(v) + case _ -> Result.Error(Error.NotFound) + } + } + + let mut target = @path + + match @path.strip_prefix(HOME_WITH_SEPARATOR) { + case Some(tail) -> { + target = match home_directory { + case Some(v) -> join_strings(v.path, tail) + case _ -> throw Error.NotFound + } + } + case _ -> {} + } + + match inko_path_expand(_INKO.state, target) { case { @tag = 0, @value = v } -> Result.Ok(Path.new(v as String)) case { @tag = _, @value = e } -> { Result.Error(Error.from_os_error(e as Int)) diff --git a/std/src/std/net/socket.inko b/std/src/std/net/socket.inko index 42171d2c5..ea7605bd5 100644 --- a/std/src/std/net/socket.inko +++ b/std/src/std/net/socket.inko @@ -87,11 +87,6 @@ class extern RawAddress { let @port: Int } -class extern AnyResult { - let @tag: Int - let @value: UInt64 -} - class extern IntResult { let @tag: Int let @value: Int @@ -222,6 +217,26 @@ fn extern inko_socket_shutdown_read_write( socket: Pointer[RawSocket], ) -> IntResult +fn extern inko_tls_socket_write( + state: Pointer[UInt8], + process: Pointer[UInt8], + socket: Pointer[RawSocket], + connection: Pointer[UInt8], + data: Pointer[UInt8], + size: Int, + deadline: Int, +) -> IntResult + +fn extern inko_tls_socket_read( + state: Pointer[UInt8], + process: Pointer[UInt8], + socket: Pointer[RawSocket], + connection: Pointer[UInt8], + buffer: mut ByteArray, + amount: Int, + deadline: Int, +) -> IntResult + # The maximum value valid for a listen() call. # # Linux and FreeBSD do not allow for values greater than this as they internally @@ -850,6 +865,50 @@ class pub Socket { panic('getsockopt(2) failed: ${Error.last_os_error}') } } + + fn pub mut tls_write_string( + con: Pointer[UInt8], + string: String, + ) -> Result[Nil, Error] { + let state = _INKO.state + let proc = _INKO.process + + match + inko_tls_socket_write( + state, + proc, + @raw, + con, + string.to_pointer, + string.size, + @deadline, + ) + { + case { @tag = 0, @value = _ } -> Result.Ok(nil) + case { @value = e } -> Result.Error(Error.from_os_error(e)) + } + } + + fn pub mut tls_read( + con: Pointer[UInt8], + into: mut ByteArray, + size: Int, + ) -> Result[Int, Error] { + match + inko_tls_socket_read( + _INKO.state, + _INKO.process, + @raw, + con, + into, + size, + @deadline, + ) + { + case { @tag = 0, @value = v } -> Result.Ok(v) + case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) + } + } } impl Drop for Socket { diff --git a/std/src/std/string.inko b/std/src/std/string.inko index 682dcfd0d..a55cf1383 100644 --- a/std/src/std/string.inko +++ b/std/src/std/string.inko @@ -482,28 +482,38 @@ class builtin String { # Returns a new `String` without the given prefix. # + # If `self` starts with the prefix, a `Option.Some` is returned containing the + # substring after the prefix. If `self` doesn't start with the prefix, an + # `Option.None` is returned. + # # # Examples # # ```inko - # 'xhellox'.strip_prefix('x') # => 'hellox' + # 'xhellox'.strip_prefix('x') # => Option.Some('hellox') + # 'xhellox'.strip_prefix('y') # => Option.None # ``` - fn pub strip_prefix(prefix: String) -> String { - if starts_with?(prefix).false? { return clone } + fn pub strip_prefix(prefix: String) -> Option[String] { + if starts_with?(prefix).false? { return Option.None } - slice(start: prefix.size, size: size - prefix.size).into_string + Option.Some(slice(start: prefix.size, size: size - prefix.size).into_string) } # Returns a new `String` without the given suffix. # + # If `self` ends with the suffix, a `Option.Some` is returned containing the + # substring before the prefix. If `self` doesn't end with the suffix, an + # `Option.None` is returned. + # # # Examples # # ```inko - # 'xhellox'.strip_suffix('x') # => 'xhello' + # 'xhellox'.strip_suffix('x') # => Option.Some('xhello') + # 'xhellox'.strip_suffix('y') # => Option.None # ``` - fn pub strip_suffix(suffix: String) -> String { - if ends_with?(suffix).false? { return clone } + fn pub strip_suffix(suffix: String) -> Option[String] { + if ends_with?(suffix).false? { return Option.None } - slice(start: 0, size: size - suffix.size).into_string + Option.Some(slice(start: 0, size: size - suffix.size).into_string) } # Returns a new `String` without any leading whitespace. diff --git a/std/test/compiler/test_diagnostics.inko b/std/test/compiler/test_diagnostics.inko index 6df68cb01..1a0336817 100644 --- a/std/test/compiler/test_diagnostics.inko +++ b/std/test/compiler/test_diagnostics.inko @@ -215,7 +215,7 @@ class Diagnostic { # We remove the directory leading up to the file, that way the diagnostic # lines in the test file don't need to specify the full file paths, and # debugging failing tests is a little less annoying due to noisy output. - let file = (try string(map, 'file')).strip_prefix('${directory}/') + let file = (try string(map, 'file')).strip_prefix('${directory}/').get let line = try location(map, 'lines') let column = try location(map, 'columns') let message = try string(map, 'message') @@ -277,7 +277,7 @@ fn pub tests(t: mut Tests) { case Error(e) -> panic('failed to read the diagnostics directory: ${e}') } - let name = test_file.tail.strip_suffix('.inko') + let name = test_file.tail.strip_suffix('.inko').get t.test('inko check ${name}', fn move (t) { let file = ReadOnlyFile.new(test_file.clone).or_panic( diff --git a/std/test/std/fs/test_path.inko b/std/test/std/fs/test_path.inko index bb0bcbdf0..6e31649e9 100644 --- a/std/test/std/fs/test_path.inko +++ b/std/test/std/fs/test_path.inko @@ -5,6 +5,7 @@ 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.sys import std.test (Tests) import std.time (DateTime) @@ -153,17 +154,37 @@ fn pub tests(t: mut Tests) { t.test('Path.fmt', fn (t) { t.equal(fmt(Path.new('foo')), '"foo"') }) t.test('Path.expand', fn (t) { - let temp = env.temporary_directory - let bar = temp.join('foo').join('bar') + with_directory(t.id, fn (temp) { + let bar = temp.join('foo').join('bar') - bar.create_directory_all.get + bar.create_directory_all.get - let expanded = bar.join('..').join('..').expand + let expanded = bar.join('..').join('..').expand - t.equal(expanded, Result.Ok(temp)) - bar.remove_directory_all + t.equal(expanded, Result.Ok(temp.clone)) + }) + + t.equal(Path.new('~').expand.ok, env.home_directory) + t.equal(Path.new('~/').expand.ok, env.home_directory) + t.true(Path.new('~foo').expand.error?) + t.true(Path.new('/~').expand.error?) + t.true(Path.new('~/this-directory-should-not-exist').expand.error?) }) + t.fork( + 'Path.expand with a missing home directory', + child: fn { + let out = STDOUT.new + let res = Path.new('~').expand.map(fn (v) { v.to_string }).or('ERROR') + + out.write_string(res) + }, + test: fn (test, proc) { + proc.variable('HOME', '') + test.equal(proc.spawn.stdout, 'ERROR') + }, + ) + t.test('Path.tail', fn (t) { t.equal(Path.new('foo').tail, 'foo') t.equal(Path.new('foo/').tail, 'foo') diff --git a/std/test/std/net/test_socket.inko b/std/test/std/net/test_socket.inko index d1c212940..eb91d97a2 100644 --- a/std/test/std/net/test_socket.inko +++ b/std/test/std/net/test_socket.inko @@ -611,7 +611,15 @@ fn pub tests(t: mut Tests) { timeout_after: Duration.from_micros(500), ) - t.equal(timed_out.error, Option.Some(Error.TimedOut)) + # If no internet connection is available the error is NetworkUnreachable + # instead, so we have to account for that. + t.true( + match timed_out { + case Error(TimedOut or NetworkUnreachable) -> true + case _ -> false + }, + ) + Result.Ok(nil) }) diff --git a/std/test/std/test_env.inko b/std/test/std/test_env.inko index f704802b0..0afadfb3e 100644 --- a/std/test/std/test_env.inko +++ b/std/test/std/test_env.inko @@ -44,6 +44,20 @@ fn pub tests(t: mut Tests) { } }) + t.fork( + 'env.home_directory with a missing home directory', + child: fn { + let out = STDOUT.new + let res = env.home_directory.map(fn (v) { v.to_string }).or('ERROR') + + out.write_string(res) + }, + test: fn (test, proc) { + proc.variable('HOME', '') + test.equal(proc.spawn.stdout, 'ERROR') + }, + ) + t.test('env.working_directory', fn (t) { let path = env.working_directory.get diff --git a/std/test/std/test_optparse.inko b/std/test/std/test_optparse.inko index d918a35f5..2bdafd9f1 100644 --- a/std/test/std/test_optparse.inko +++ b/std/test/std/test_optparse.inko @@ -535,7 +535,8 @@ fn pub tests(t: mut Tests) { lorem ipsum --example -x Foo' - .strip_prefix('\n'), + .strip_prefix('\n') + .get, ) opts.single('o', 'option-with-a-much-longer-name', '', 'Example') @@ -552,7 +553,8 @@ fn pub tests(t: mut Tests) { --example -x Foo -o, --option-with-a-much-longer-name Example' - .strip_prefix('\n'), + .strip_prefix('\n') + .get, ) }) diff --git a/std/test/std/test_string.inko b/std/test/std/test_string.inko index a83446645..9483547ef 100644 --- a/std/test/std/test_string.inko +++ b/std/test/std/test_string.inko @@ -258,27 +258,27 @@ fn pub tests(t: mut Tests) { }) t.test('String.strip_prefix', fn (t) { - t.equal('hello'.strip_prefix('xxxxxxxxx'), 'hello') - t.equal('hello'.strip_prefix('x'), 'hello') - t.equal('hello'.strip_prefix(''), 'hello') - t.equal('XhelloX'.strip_prefix('x'), 'XhelloX') - t.equal('xhellox'.strip_prefix('xy'), 'xhellox') - t.equal('xhellox'.strip_prefix('y'), 'xhellox') - t.equal('xhellox'.strip_prefix('x'), 'hellox') - t.equal('xxhelloxx'.strip_prefix('xx'), 'helloxx') - t.equal('๐Ÿ˜ƒhello๐Ÿ˜ƒ'.strip_prefix('๐Ÿ˜ƒ'), 'hello๐Ÿ˜ƒ') + t.equal('hello'.strip_prefix('xxxxxxxxx'), Option.None) + t.equal('hello'.strip_prefix('x'), Option.None) + t.equal('hello'.strip_prefix(''), Option.None) + t.equal('XhelloX'.strip_prefix('x'), Option.None) + t.equal('xhellox'.strip_prefix('xy'), Option.None) + t.equal('xhellox'.strip_prefix('y'), Option.None) + t.equal('xhellox'.strip_prefix('x'), Option.Some('hellox')) + t.equal('xxhelloxx'.strip_prefix('xx'), Option.Some('helloxx')) + t.equal('๐Ÿ˜ƒhello๐Ÿ˜ƒ'.strip_prefix('๐Ÿ˜ƒ'), Option.Some('hello๐Ÿ˜ƒ')) }) t.test('String.strip_suffix', fn (t) { - t.equal('hello'.strip_suffix('xxxxxxxxx'), 'hello') - t.equal('hello'.strip_suffix('x'), 'hello') - t.equal('hello'.strip_suffix(''), 'hello') - t.equal('XhelloX'.strip_suffix('x'), 'XhelloX') - t.equal('xhellox'.strip_suffix('xy'), 'xhellox') - t.equal('xhellox'.strip_suffix('y'), 'xhellox') - t.equal('xhellox'.strip_suffix('x'), 'xhello') - t.equal('xxhelloxx'.strip_suffix('xx'), 'xxhello') - t.equal('๐Ÿ˜ƒhello๐Ÿ˜ƒ'.strip_suffix('๐Ÿ˜ƒ'), '๐Ÿ˜ƒhello') + t.equal('hello'.strip_suffix('xxxxxxxxx'), Option.None) + t.equal('hello'.strip_suffix('x'), Option.None) + t.equal('hello'.strip_suffix(''), Option.None) + t.equal('XhelloX'.strip_suffix('x'), Option.None) + t.equal('xhellox'.strip_suffix('xy'), Option.None) + t.equal('xhellox'.strip_suffix('y'), Option.None) + t.equal('xhellox'.strip_suffix('x'), Option.Some('xhello')) + t.equal('xxhelloxx'.strip_suffix('xx'), Option.Some('xxhello')) + t.equal('๐Ÿ˜ƒhello๐Ÿ˜ƒ'.strip_suffix('๐Ÿ˜ƒ'), Option.Some('๐Ÿ˜ƒhello')) }) t.test('String.trim_start', fn (t) {