Index: .efiles ================================================================== --- .efiles +++ .efiles @@ -5,17 +5,18 @@ src/err.rs src/lib.rs src/path.rs src/fs.rs src/strx.rs +src/numx.rs src/buf.rs -src/sqlfuncs.rs src/futures.rs src/iox.rs src/tokiox.rs -src/tokiox/tcpconn.rs src/serde_parsers.rs src/setops.rs +src/ffi.rs tests/setops.rs examples/fut_if_some.rs examples/deser.rs examples/slasher.rs +examples/diskfree.rs Index: Cargo.toml ================================================================== --- Cargo.toml +++ Cargo.toml @@ -1,8 +1,8 @@ [package] name = "orphanage" -version = "0.1.4" +version = "0.2.0" edition = "2021" license = "0BSD" # https://crates.io/category_slugs categories = [ "network-programming" ] keywords = [ "sqlite", "fs", "path" ] @@ -22,33 +22,43 @@ # https://doc.rust-lang.org/cargo/reference/manifest.html#the-badges-section [badges] maintenance = { status = "experimental" } [features] -tokio = ["dep:tokio", "dep:async-trait", "dep:killswitch"] -rusqlite = ["dep:rusqlite", "dep:sha2"] +tokio = ["dep:tokio"] serde = ["dep:serde", "dep:parse-size"] [dependencies] -async-trait = { version = "0.1.82", optional = true } -hashbrown = { version = "0.15.1" } -killswitch = { version = "0.4.2", optional = true } +#async-trait = { version = "0.1.86", optional = true } +hashbrown = { version = "0.15.2" } parse-size = { version = "1.1.0", optional = true } -rand = { version = "0.8.5" } -rusqlite = { version = "0.32.1", optional = true, features = ["functions"] } -serde = { version = "1.0.214", optional = true, features = ["derive"] } -sha2 = { version = "0.10.7", optional = true } +paste = { version = "1.0.15" } +rand = { version = "0.9.0" } +serde = { version = "1.0.218", optional = true, features = ["derive"] } +sha2 = { version = "0.10.8", optional = true } +sha3 = { version = "0.10.8", optional = true } shellexpand = { version = "3.1.0" } tokio = { version = "1.41.0", optional = true, features = [ "macros", "net", "time" ] } + +[target.'cfg(unix)'.dependencies] +libc = { version = "0.2.171" } + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.59.0", features = [ + "Win32_Foundation", + "Win32_Storage", + "Win32_Storage_FileSystem" +] } [dev-dependencies] +humansize = { version = "2.1.3" } killswitch = { version = "0.4.2" } -tokio = { version = "1.40.0", features = ["full"] } +tokio = { version = "1.43.0", features = ["full"] } tokio-test = { version = "0.4.3" } -toml = { version = "0.8.18" } +toml = { version = "0.8.20" } [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"] ADDED examples/diskfree.rs Index: examples/diskfree.rs ================================================================== --- /dev/null +++ examples/diskfree.rs @@ -0,0 +1,27 @@ +use std::{env, path::Path}; + +use humansize::{format_size, BINARY, DECIMAL}; + +use orphanage::fs::get_free_space; + + +fn main() { + let pth = env::args_os().nth(1).expect("no pattern given"); + + let pth = Path::new(&pth); + + let freespace = get_free_space(pth).unwrap(); + + println!("{freespace}"); + println!("{}", freespace / 1024); + println!("{}", freespace / 1024 / 1024); + + + let hs: String = format_size(freespace, DECIMAL); + println!("{hs}"); + + let hs: String = format_size(freespace, BINARY); + println!("{hs}"); +} + +// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/buf.rs ================================================================== --- src/buf.rs +++ src/buf.rs @@ -13,11 +13,11 @@ // SAFETY: Presumably with_capacity() works as documented. unsafe { buf.set_len(len); } - rand::thread_rng().fill(&mut buf[..]); + rand::rng().fill(&mut buf[..]); buf } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : ADDED src/ffi.rs Index: src/ffi.rs ================================================================== --- /dev/null +++ src/ffi.rs @@ -0,0 +1,19 @@ +use std::{ffi::CString, path::Path}; + +/// Given a `Path`, return a `CString`, suitable for passing to C functions +/// that require null-terminated paths. +/// +/// # Panics +/// Path must not include null's (which they shouldn't be able to). +#[must_use] +#[inline] +pub fn path_to_cstring(path: &Path) -> CString { + let os_str = path.as_os_str(); + let bytes = os_str.as_encoded_bytes(); + + // unwrap() should be okay because embeded null shouldn't be possible in a + // Path. + CString::new(bytes).unwrap() +} + +// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/fs.rs ================================================================== --- src/fs.rs +++ src/fs.rs @@ -2,11 +2,14 @@ fs, io::ErrorKind, path::{Path, PathBuf} }; -use crate::err::Error; +#[cfg(windows)] +use windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExA; + +use crate::{err::Error, ffi::path_to_cstring}; /// Create a random file of a specified size. /// /// # Errors /// [`std::io::Error`] @@ -64,11 +67,10 @@ P: AsRef { Ok(pth.as_ref().canonicalize()?) } - /// Call a closure if `outfile` is outdated (according to the rules of /// [`outdated()`] with regards to `infile`. /// /// Returns `Ok(true)` if the closure was called. /// @@ -117,6 +119,81 @@ let out_mtime = out_md.modified()?; Ok(in_mtime > out_mtime) } + +/// Given a directory, return how much disk space is available. +/// +/// The directory must exist. +/// +/// # Errors +/// [`Error::IO`] is returned if the input path is not a directory or if the +/// free space could not be probed. +pub fn get_free_space(path: &Path) -> Result { + #[cfg(unix)] + let res = get_free_space_unix(path); + + #[cfg(windows)] + let res = get_free_space_win(path); + + res +} + + +#[cfg(unix)] +fn get_free_space_unix(path: &Path) -> Result { + // Make sure directory exists + if !path.is_dir() { + return Err(Error::IO("Not a directory".into())); + } + + // Must construct a null-terminated C string for libc::statfs() + let cstr = path_to_cstring(path); + let cstr_path = cstr.as_bytes_with_nul(); + + let mut statfs = unsafe { std::mem::zeroed() }; + let result = + unsafe { libc::statfs(cstr_path.as_ptr().cast::(), &mut statfs) }; + + if result == 0 { + Ok(statfs.f_bavail as u64 * u64::from(statfs.f_bsize)) + } else { + Err(Error::IO("statfs() failed".into())) + } +} + +#[cfg(windows)] +fn get_free_space_win(path: &Path) -> Result { + // Make sure directory exists + if !path.is_dir() { + return Err(Error::IO("Not a directory".into())); + } + + // Need a null-terminated string for ffi + let cstr = path_to_cstring(path); + let cstr_path = cstr.as_bytes_with_nul(); + + let mut free_bytes_available_to_caller: u64 = 0; + let mut total_bytes: u64 = 0; + let mut total_free_bytes: u64 = 0; + + let result = unsafe { + GetDiskFreeSpaceExA( + cstr_path.as_ptr(), + &mut free_bytes_available_to_caller, + &mut total_bytes, + &mut total_free_bytes + ) + }; + + if result != 0 { + Ok(free_bytes_available_to_caller) + } else { + Err(Error::IO(format!( + "GetDiskFreeSpaceExW failed with code: {}", + std::io::Error::last_os_error() + ))) + } +} + // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/iox.rs ================================================================== --- src/iox.rs +++ src/iox.rs @@ -26,16 +26,16 @@ // signal eof Ok(0) } else { let n = std::cmp::min(*remain, buf.len() as u64); let len = usize::try_from(n).unwrap(); - rand::thread_rng().fill(&mut buf[..len]); + rand::rng().fill(&mut buf[..len]); *remain -= n; Ok(len) } } else { - rand::thread_rng().fill(&mut buf[..]); + rand::rng().fill(&mut buf[..]); Ok(buf.len()) } } } Index: src/lib.rs ================================================================== --- src/lib.rs +++ src/lib.rs @@ -1,28 +1,22 @@ #![cfg_attr(docsrs, feature(doc_cfg))] pub mod buf; mod err; +pub mod ffi; pub mod fs; pub mod futures; pub mod iox; +pub mod numx; pub mod path; pub mod setops; pub mod strx; -#[cfg(feature = "rusqlite")] -#[cfg_attr(docsrs, doc(cfg(feature = "rusqlite")))] -pub mod sqlfuncs; - #[cfg(feature = "tokio")] #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] pub mod tokiox; -#[cfg(feature = "tokio")] -#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] -pub use async_trait::async_trait; - #[cfg(feature = "serde")] #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] pub mod serde_parsers; pub use err::Error; ADDED src/numx.rs Index: src/numx.rs ================================================================== --- /dev/null +++ src/numx.rs @@ -0,0 +1,232 @@ +use paste::paste; + +pub trait UnsignedExt { + type Type; + + /// Round down integer to its closes power-of-two. + fn round_down_to_pow2(self) -> Self::Type; + + /// Generate a vector of all bit indexes containing a `1`. + fn bitidx_vec(self) -> Vec; +} + + +/// Given an array of (sorted) indexes, generate a vector of contiguous blocks. +/// +/// Each block is inclusive-inclusive. +#[must_use] +pub fn usize_list_to_ranges(numbers: &[usize]) -> Vec<(usize, usize)> { + let mut result = Vec::with_capacity(numbers.len()); + + if numbers.is_empty() { + return result; + } + + let mut numbers_iter = numbers.iter().peekable(); + + while let Some(&num) = numbers_iter.next() { + let start = num; + let mut end = num; + + while let Some(&&next_num) = numbers_iter.peek() { + if next_num == end + 1 { + end = next_num; + numbers_iter.next(); + } else { + break; + } + } + + if start == end { + result.push((start, start)); + } else { + result.push((start, end)); + } + } + + result +} + +#[must_use] +pub fn ranges_to_str(blocks: &[(usize, usize)]) -> String { + let mut res = String::new(); + if blocks.is_empty() { + return res; + } + + let mut it = blocks.iter().peekable(); + + while let Some((start, end)) = it.next() { + if start == end { + res.push_str(&start.to_string()); + } else { + res.push_str(&format!("{start}-{end}")); + } + + if it.peek().is_some() { + res.push(','); + } + } + + res +} + + +macro_rules! impl_unsigned_ext { + ($int_type:ident) => { + paste! { + #[must_use] + pub const fn [<$int_type _round_down_to_pow2>](val: $int_type) + -> $int_type { + if val == 0 { + return 0; + } + let n = val.leading_zeros(); + let nshift = $int_type::BITS - n - 1; + 1 << nshift + } + } + + paste! { + #[must_use] + pub fn [<$int_type _bitidx_vec>](bits: $int_type) -> Vec { + let nbits = $int_type::BITS as usize; + let mut ret = Vec::with_capacity(nbits); + for idx in 0..nbits { + if bits & (1 << idx) != 0 { + ret.push(idx + 1); + } + } + ret + } + } + + paste! { + impl UnsignedExt for $int_type { + type Type = $int_type; + + #[inline] + fn round_down_to_pow2(self) -> Self::Type { + [<$int_type _round_down_to_pow2>](self) + } + + #[inline] + fn bitidx_vec(self) -> Vec { + [<$int_type _bitidx_vec>](self) + } + } + } + }; +} + +impl_unsigned_ext! { u8 } +impl_unsigned_ext! { u16 } +impl_unsigned_ext! { u32 } +impl_unsigned_ext! { u64 } +impl_unsigned_ext! { usize } + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bitidxvec() { + let v = u32_bitidx_vec(0); + assert_eq!(v.len(), 0); + + let v = u32_bitidx_vec(1); + assert_eq!(&v, &[1]); + + let v = u32_bitidx_vec(2); + assert_eq!(&v, &[2]); + + let v = u32_bitidx_vec(3); + assert_eq!(&v, &[1, 2]); + + let v = u32_bitidx_vec(4); + assert_eq!(&v, &[3]); + } + + #[test] + fn to_range_blocks() { + let v = u32_bitidx_vec(0); + let v2 = usize_list_to_ranges(&v); + assert_eq!(v2.len(), 0); + + let v = u32_bitidx_vec(1); + let v2 = usize_list_to_ranges(&v); + assert_eq!(&v2, &[(1, 1)]); + + let v = u32_bitidx_vec(2); + let v2 = usize_list_to_ranges(&v); + assert_eq!(&v2, &[(2, 2)]); + + let v = u32_bitidx_vec(3); + let v2 = usize_list_to_ranges(&v); + assert_eq!(&v2, &[(1, 2)]); + } + + #[test] + fn to_ranges_str() { + let v = u32_bitidx_vec(0); + let v2 = usize_list_to_ranges(&v); + let s = ranges_to_str(&v2); + assert!(s.is_empty()); + + let v = u32_bitidx_vec(1); + let v2 = usize_list_to_ranges(&v); + let s = ranges_to_str(&v2); + assert_eq!(&s, "1"); + + let v = u32_bitidx_vec(2); + let v2 = usize_list_to_ranges(&v); + let s = ranges_to_str(&v2); + assert_eq!(&s, "2"); + + let v = u32_bitidx_vec(3); + let v2 = usize_list_to_ranges(&v); + let s = ranges_to_str(&v2); + assert_eq!(&s, "1-2"); + + let v = u32_bitidx_vec(4); + let v2 = usize_list_to_ranges(&v); + let s = ranges_to_str(&v2); + assert_eq!(&s, "3"); + + let v = u32_bitidx_vec(5); + let v2 = usize_list_to_ranges(&v); + let s = ranges_to_str(&v2); + assert_eq!(&s, "1,3"); + + let v = u32_bitidx_vec(1 + 2 + 8 + 16); + let v2 = usize_list_to_ranges(&v); + let s = ranges_to_str(&v2); + assert_eq!(&s, "1-2,4-5"); + } + + #[test] + fn round_down_to_pow2() { + assert_eq!(u8::round_down_to_pow2(0), 0); + assert_eq!(u8::round_down_to_pow2(1), 1); + assert_eq!(u8::round_down_to_pow2(2), 2); + assert_eq!(u8::round_down_to_pow2(3), 2); + + assert_eq!(u16::round_down_to_pow2(0), 0); + assert_eq!(u16::round_down_to_pow2(1), 1); + assert_eq!(u16::round_down_to_pow2(2), 2); + assert_eq!(u16::round_down_to_pow2(3), 2); + + assert_eq!(u32::round_down_to_pow2(0), 0); + assert_eq!(u32::round_down_to_pow2(1), 1); + assert_eq!(u32::round_down_to_pow2(2), 2); + assert_eq!(u32::round_down_to_pow2(3), 2); + + assert_eq!(u64::round_down_to_pow2(0), 0); + assert_eq!(u64::round_down_to_pow2(1), 1); + assert_eq!(u64::round_down_to_pow2(2), 2); + assert_eq!(u64::round_down_to_pow2(3), 2); + } +} + +// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : DELETED src/sqlfuncs.rs Index: src/sqlfuncs.rs ================================================================== --- src/sqlfuncs.rs +++ /dev/null @@ -1,198 +0,0 @@ -use rusqlite::{functions::FunctionFlags, Connection, Error}; - -use sha2::{Digest, Sha256}; - -use crate::strx::{validate_objname, RndStr}; - -/// Add a `hashstr()` SQL function to the connection object. -/// -/// The SQL function `hashstr()` takes two arguments: -/// 1. The algorithm that will be used to hash the input. -/// 2. The input string to hash. -/// -/// Currently only the algorithm `sha2/256` is supported. -/// -/// ``` -/// use rusqlite::Connection; -/// use orphanage::sqlfuncs; -/// -/// let conn = Connection::open_in_memory().unwrap(); -/// sqlfuncs::hashstr(&conn).unwrap(); -/// let instr = "hello, world"; -/// let hstr: String = conn.query_row_and_then( -/// "SELECT lower(hex(hashstr('sha2/256', ?)));", -/// [instr], -/// |row| { -/// row.get(0) -/// } -/// ).unwrap(); -/// assert_eq!(&hstr[..8], "09ca7e4e"); -/// ``` -#[allow(clippy::missing_errors_doc)] -pub fn hashstr(conn: &Connection) -> Result<(), Error> { - conn.create_scalar_function( - "hashstr", - 2, - FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC, - move |ctx| { - //assert_eq!(ctx.len(), 2, "called with unexpected number of - // arguments"); - let algo = ctx.get::(0)?.to_lowercase(); - let s = ctx.get::(1)?; - - let mut hasher = match algo.as_ref() { - "sha2/256" => Sha256::new(), - _ => Err(Error::UserFunctionError("Bad hash algo".into()))? - }; - hasher.update(s.as_bytes()); - let result = hasher.finalize(); - - Ok(result[..].to_vec()) - } - ) -} - -/// Add a `hashblob()` SQL function to the connection object. -/// -/// The SQL function `hashblob()` takes two arguments: -/// 1. The algorithm that will be used to hash the input. -/// 2. The input string to hash. -/// -/// Currently only the algorithm `sha2/256` is supported. -/// -/// ``` -/// use rusqlite::Connection; -/// use orphanage::sqlfuncs; -/// -/// let conn = Connection::open_in_memory().unwrap(); -/// sqlfuncs::hashblob(&conn).unwrap(); -/// let buf = b"hello, world"; -/// let hstr: String = conn.query_row_and_then( -/// "SELECT lower(hex(hashblob('sha2/256', ?)));", -/// [&buf], -/// |row| { -/// row.get(0) -/// } -/// ).unwrap(); -/// assert_eq!(&hstr[..8], "09ca7e4e"); -/// ``` -#[allow(clippy::missing_errors_doc)] -pub fn hashblob(conn: &Connection) -> Result<(), Error> { - conn.create_scalar_function( - "hashblob", - 2, - FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC, - move |ctx| { - //assert_eq!(ctx.len(), 2, "called with unexpected number of - // arguments"); - let algo = ctx.get::(0)?.to_lowercase(); - let blob = ctx.get::>(1)?; - - let mut hasher = match algo.as_ref() { - "sha2/256" => Sha256::new(), - _ => Err(Error::UserFunctionError("Bad hash algo".into()))? - }; - hasher.update(&blob); - let result = hasher.finalize(); - - Ok(result[..].to_vec()) - } - ) -} - -/// Add a `randomstr()` SQL function to the connection object. -/// -/// The SQL function `randomstr()` takes a single argument: -/// 1. The length of the random string, in characters. -/// -/// Its output will be a random string of the requested length where each -/// character is a ASCII alphabet or numeric character. -/// -/// ``` -/// use rusqlite::Connection; -/// use orphanage::sqlfuncs; -/// -/// let conn = Connection::open_in_memory().unwrap(); -/// sqlfuncs::rndstr_alphanum(&conn).unwrap(); -/// -/// conn.execute(r#" -/// CREATE TABLE IF NOT EXISTS stuff ( -/// id INTEGER PRIMARY KEY, -/// salt TEXT NOT NULL DEFAULT (randomstr(8)) -/// ); -/// "#, []); -/// ``` -#[allow(clippy::missing_errors_doc)] -pub fn rndstr_alphanum(db: &Connection) -> Result<(), Error> { - db.create_scalar_function( - "randomstr", - 1, - FunctionFlags::SQLITE_UTF8, - move |ctx| { - //assert_eq!(ctx.len(), 1, "called with unexpected number of - // arguments"); - let len = ctx.get::(0)?; - - Ok(String::rnd_alphanum(len)) - } - ) -} - -#[allow(clippy::missing_errors_doc)] -pub fn rndstr(db: &Connection) -> Result<(), Error> { - db.create_scalar_function( - "randomstr", - 2, - FunctionFlags::SQLITE_UTF8, - move |ctx| { - //assert_eq!(ctx.len(), 2, "called with unexpected number of - // arguments"); - let len = ctx.get::(0)?; - let alphabet = ctx.get::(1)?; - let charset = alphabet.as_bytes(); - - Ok(String::rnd_from_alphabet(len, charset)) - } - ) -} - -/// Add a `isobjname()` SQL function to the connection object. -/// -/// The SQL function `isobjname()` takes a signle argument: -/// 1. The input string to check whether it conforms to an object name. -/// -/// ``` -/// use rusqlite::Connection; -/// use orphanage::sqlfuncs; -/// -/// let conn = Connection::open_in_memory().unwrap(); -/// sqlfuncs::hashstr(&conn).unwrap(); -/// -/// conn.execute(r#" -/// CREATE TABLE IF NOT EXISTS stuff ( -/// id INTEGER PRIMARY KEY, -/// name TEXT UNIQUE NOT NULL, -/// CHECK (isobjname(name) == 1) -/// ); -/// "#, []); -/// ``` -/// -/// # Panics -/// The number of input parameters must be exactly 1. -#[allow(clippy::missing_errors_doc)] -pub fn isobjname(conn: &Connection) -> Result<(), Error> { - conn.create_scalar_function( - "isobjname", - 1, - FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC, - move |ctx| { - assert_eq!(ctx.len(), 1, "called with unexpected number of arguments"); - let name = ctx.get::(0)?; - validate_objname(&name) - .map_err(|e| Error::UserFunctionError(Box::new(e)))?; - Ok(1) - } - ) -} - -// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/strx.rs ================================================================== --- src/strx.rs +++ src/strx.rs @@ -1,20 +1,18 @@ //! Extended str/string functionality. -use rand::{distributions::Alphanumeric, Rng}; - -use crate::err::Error; +use rand::{distr::Alphanumeric, Rng}; pub trait RndStr { fn rnd_alphanum(len: usize) -> String; fn rnd_from_alphabet(len: usize, alpha: &[u8]) -> String; } impl RndStr for String { /// Generate a random alphanumeric string of a requested length. fn rnd_alphanum(len: usize) -> String { - rand::thread_rng() + rand::rng() .sample_iter(&Alphanumeric) .take(len) .map(char::from) .collect() } @@ -31,14 +29,14 @@ /// /// let s = String::rnd_from_alphabet(16, CHARSET); /// assert_eq!(s.len(), 16); /// ``` fn rnd_from_alphabet(len: usize, charset: &[u8]) -> String { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); (0..len) .map(|_| { - let idx = rng.gen_range(0..charset.len()); + let idx = rng.random_range(0..charset.len()); charset[idx] as char }) .collect() } } @@ -61,37 +59,31 @@ character, with each following character \ being alphanumeric, '_', '-' or '.'"; #[allow(clippy::missing_errors_doc)] -pub fn validate_name(s: &str, lead: L, rest: R) -> Result<(), Error> +pub fn validate_name(s: &str, lead: L, rest: R) -> Result<&str, String> where L: Fn(char) -> bool, R: Fn(char) -> bool { let mut chars = s.chars(); let Some(ch) = chars.next() else { - return Err(Error::BadFormat( - "Object name must not be empty".to_string() - )); + return Err("must not be empty".into()); }; if !lead(ch) { - return Err(Error::BadFormat( - "Invalid leading object name character".to_string() - )); + return Err("invalid leading character".into()); } if chars.any(|c| !rest(c)) { - return Err(Error::BadFormat( - "Invalid object name character".to_string() - )); + return Err("invalid character".into()); } - Ok(()) + Ok(s) } #[inline] #[allow(clippy::missing_errors_doc)] -pub fn validate_objname(s: &str) -> Result<(), Error> { +pub fn validate_objname(s: &str) -> Result<&str, String> { validate_name(s, is_name_leading_char, is_name_char) } #[cfg(test)] Index: src/tokiox.rs ================================================================== --- src/tokiox.rs +++ src/tokiox.rs @@ -1,159 +1,6 @@ //! tokio extensions. -//! -//! # Connector -//! The _Connector_ subsystem consists of two parts: -//! - The [`Connector`] trait is implemented by applications/libraries that -//! want to run retryable connection loops. -//! - [`run_connector()`] is a function that takes in a `Connector` -//! implementation, and attempts to establish a connection and calls the -//! [`Connector::run()`] once a connection has been successfully been -//! established. -//! -//! Paradoxically the `run_connector()` function does not itself actually -//! attemtp to establish any connections -- it relies on the `Connector` trait -//! to implement the means to establish connections. -//! -//! The "good path" overall flow of the connector loop is to call the -//! `connect()` method. If it is successful, call the `run()` method, passing -//! along the newly allocated connection. The main application logic relating -//! to the connection should called from this method. -//! -//! The primary purpose of the connector concerns the "failure path": If the -//! `connect()` method encounters a failure it can choose to signal to back to -//! the connector loop that the error is "retryable", in which case the -//! `retry_delay()` method is called to determine if the connector loop should -//! retry (and implement a delay before returning instructions to do so). -//! -//! Likewise, the `run()` method returns its `Result<(), Self::Error>` wrapped -//! in a `ControlFlow::Continue(_)` to indicate that the connector look should -//! reconnect, while `ControlFlow::Break(_)` signals that a fatal error occured -//! and the connect loop should terminate. - -pub mod tcpconn; - -use std::ops::ControlFlow; - -use async_trait::async_trait; - -pub use tcpconn::SimpleTcpConnector; - - -/// Application callbacks for the [`run_connector()`] function (or equivalent). -#[async_trait] -pub trait Connector { - /// The connection return type. - type ConnType: Send; - - /// The application return type. - type Error; - - /// Establish a connection. - /// - /// If a connection was successfully established the implementation should - /// return `Ok(Self::ConnType)`. - /// - /// If an error is returned as `Err(ControlFlow::Continue(Self::Error))` it - /// signals to the connector loop that the error is non-fatal, and that the - /// connection should be retried. Returning an error as - /// `Err(ControlFlow::Break(Self::Error))` signals that there's no point in - /// trying to (re)connect (with the same configuration) and the - /// (re)connection loop is terminated. - async fn connect( - &mut self - ) -> Result>; - - /// Give application a chance to determine whether or not to attempt a - /// reconnection, and delay before doing so. - /// - /// Implementations return `ControlFlow::Continue(())` to signal to the - /// connector loop that it should retry the connection. Returnning - /// `ControlFlow::Break(Self::Error)` will terminate the connector loop and - /// cause it to return the error. - async fn retry_delay(&mut self) -> ControlFlow; - - /// Run the application's connection handler. - /// - /// The application should return `ControlFlow::Continue(_)` to request that - /// the connector loop delay and reconnect. Returning - /// `ControlFlow::Break(_)` will cause the connect loop to terminate and - /// return the suppied result. - async fn run( - &mut self, - conn: Self::ConnType - ) -> ControlFlow, Result<(), Self::Error>>; -} - - -/// Establish a network connection. -/// -/// The `run_connector()` function will enter a loop that will attempt to -/// establish a connection and call the `Connector::run()` implementation once -/// succesful. If the connection fails, `Connector::retry_delay()` will be -/// called to determine whether to retry the connection. -/// -/// # Exit conditions -/// The (re)connection loop will keep running until an exit condition has been -/// triggered: -/// - [`Connector::connect()`] returns `Err(ControlFlow::Break(Self::Error))` -/// - [`Connector::retry_delay()`] returns `ControlFlow::Break(Self::Error)`. -/// - [`Connector::run()`] returns `ControlFlow::Break(Result<(), -/// Self::Error>)` -#[deprecated(since = "0.0.3", note = "use `schmoozer` crate instead")] -#[allow(clippy::missing_errors_doc)] -pub async fn run_connector( - mut connector: impl Connector + Send -) -> Result<(), E> -where - E: Send -{ - loop { - // Call the application's connect callback to attempt to establish - // connection. - match connector.connect().await { - Ok(conn) => { - // A connection was successfully established -- call the run() - // implementation. - match connector.run(conn).await { - ControlFlow::Continue(_res) => { - // The application has requested a reconnection. - // Fall through to retry()/delay() - } - ControlFlow::Break(res) => { - // Break out of loop -- passing along the result from the - // application. - break res; - } - } - } - Err(ControlFlow::Continue(_res)) => { - // The connector returned a retriable error - // fall through to retry()/delay() - } - Err(ControlFlow::Break(res)) => { - // The connector returned a fatal error - break Err(res); - } - } - - // If this point is reached the application has requested a reconnection. - // Call `retry_delay()` to allow the application to determine whether to - // retry or not. - - match connector.retry_delay().await { - ControlFlow::Continue(()) => { - // Application wants to reconnect. - continue; - } - ControlFlow::Break(err) => { - // Application does not want to reconnect - break Err(err); - } - } - } -} - /// If a timeout has been specified, sleep until then. /// /// Otherwise wait forever. /// DELETED src/tokiox/tcpconn.rs Index: src/tokiox/tcpconn.rs ================================================================== --- src/tokiox/tcpconn.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::{future::Future, io::ErrorKind, ops::ControlFlow, time::Duration}; - -use tokio::net::TcpStream; - -use async_trait::async_trait; - -use killswitch::KillSwitch; - -use super::Connector; - -pub struct SimpleTcpConnector -where - F: Future< - Output = ControlFlow< - Result<(), std::io::Error>, - Result<(), std::io::Error> - > - > -{ - addr: String, - delay: usize, - ks: KillSwitch, - cb: Box F + Send> -} - -impl SimpleTcpConnector -where - F: Future< - Output = ControlFlow< - Result<(), std::io::Error>, - Result<(), std::io::Error> - > - > -{ - #[must_use] - pub fn new( - addr: String, - ks: KillSwitch, - cb: Box F + Send> - ) -> Self { - Self { - addr, - delay: 1, - ks, - cb - } - } -} - -#[async_trait] -impl Connector for SimpleTcpConnector -where - F: Future< - Output = ControlFlow< - Result<(), std::io::Error>, - Result<(), std::io::Error> - > - > + Send -{ - type Error = std::io::Error; - type ConnType = TcpStream; - - async fn connect( - &mut self - ) -> Result> { - tokio::select! { - res = TcpStream::connect(&self.addr) => { - match res { - Ok(conn) => Ok(conn), - Err(e) => match e.kind() { - ErrorKind::ConnectionAborted | ErrorKind::NotConnected => { - Err(ControlFlow::Continue(e)) - } - _ => Err(ControlFlow::Break(e)) - } - } - } - () = self.ks.wait() => { - // Aborted -- use ErrorKind::Other to signal abortion - let err = std::io::Error::other(String::from("aborted")); - Err(ControlFlow::Break(err)) - } - } - } - - async fn retry_delay(&mut self) -> ControlFlow { - let dur = Duration::from_secs(self.delay.try_into().unwrap()); - tokio::select! { - () = self.ks.wait() => { - let err = std::io::Error::other(String::from("aborted")); - ControlFlow::Break(err) - } - () = tokio::time::sleep(dur) => { - // double sleep duration for each iteration, but cap at 60 seconds - self.delay = std::cmp::min(self.delay * 2, 60); - ControlFlow::Continue(()) - } - } - } - - async fn run( - &mut self, - conn: Self::ConnType - ) -> ControlFlow, Result<(), Self::Error>> { - // reset delay - self.delay = 1; - - let fut = (self.cb)(conn, self.ks.clone()); - - fut.await - } -} - -// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: www/changelog.md ================================================================== --- www/changelog.md +++ www/changelog.md @@ -1,17 +1,44 @@ # Change Log +⚠️ indicates a breaking change. + ## [Unreleased] -[Details](/vdiff?from=orphanage-0.1.4&to=trunk) +[Details](/vdiff?from=orphanage-0.2.0&to=trunk) + +### Added + +### Changed + +### Removed + +--- + +## [0.2.0] - 2025-04-03 + +[Details](/vdiff?from=orphanage-0.1.4&to=orphanage-0.2.0) ### Added + +- Add `ffi` module with a `path_to_cstring()` function. ### Changed +- Upgrade `rand` to `0.9.0`. +- ⚠️ Change `validate_name()` and `validate_objname` to return + `Result<(), String>` with a more constrained message. + ### Removed +- ⚠️ Removed `sqlfuncs` module (it lives in + [sqlfuncs](https://crates.io/crates/sqlfuncs) now) +- Removed rusqlite dependency (as a consequence of sqlfuncs being removed) +- ⚠️ Remove `tokiox::{Connector, run_connector, tcpconn}`. The connector lives + in [schmoozer](https://crates.io/crates/schmoozer) now. +- ⚠️ Remove `aync_trait` dependency (it used to be re-exported). + --- ## [0.1.4] - 2024-11-22 [Details](/vdiff?from=orphanage-0.1.3&to=orphanage-0.1.4) Index: www/index.md ================================================================== --- www/index.md +++ www/index.md @@ -17,5 +17,12 @@ ``` RUSTFLAGS="--cfg docsrs" RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features ``` + +## Change log + +The details of changes can always be found in the timeline, but for a +high-level view of changes between released versions there's a manually +maintained [Change Log](./changelog.md). +