diff --git a/.circleci/config.yml b/.circleci/config.yml index aa13f8bd..3e964086 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -129,6 +129,12 @@ jobs: --manifest-path=systest/Cargo.toml \ <<# parameters.vendored >>--features vendored<> \ --target << parameters.target >> + - run: | + cargo test \ + --manifest-path=openssl-errors/Cargo.toml \ + <<# parameters.vendored >>--features openssl-sys/vendored<> \ + --target << parameters.target >> \ + <<# parameters.no_run >>--no-run<> - run: | ulimit -c unlimited export PATH=$OPENSSL_DIR/bin:$PATH @@ -176,6 +182,10 @@ jobs: cargo run \ --manifest-path=systest/Cargo.toml \ <<# parameters.vendored >> --features vendored <> + - run: | + cargo run \ + --manifest-path=openssl-errors/Cargo.toml \ + <<# parameters.vendored >> --features openssl-sys/vendored <> - run: | PATH=/usr/local/opt/openssl/bin:$PATH cargo test \ diff --git a/Cargo.toml b/Cargo.toml index 2ef99c17..3ad379d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,7 @@ [workspace] -members = ["openssl", "openssl-sys", "systest"] +members = [ + "openssl", + "openssl-errors", + "openssl-sys", + "systest", +] diff --git a/openssl-errors/Cargo.toml b/openssl-errors/Cargo.toml new file mode 100644 index 00000000..9f08cf78 --- /dev/null +++ b/openssl-errors/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "openssl-errors" +version = "0.1.0" +authors = ["Steven Fackler "] +edition = "2018" + +[dependencies] +libc = "0.2" + +openssl-sys = { version = "0.9.42", path = "../openssl-sys" } + +[dev-dependencies] +openssl = { version = "0.10.19", path = "../openssl" } diff --git a/openssl-errors/src/lib.rs b/openssl-errors/src/lib.rs new file mode 100644 index 00000000..01a616d5 --- /dev/null +++ b/openssl-errors/src/lib.rs @@ -0,0 +1,213 @@ +use libc::{c_char, c_int}; +use std::ptr; +use std::borrow::Cow; +use std::marker::PhantomData; + +#[doc(hidden)] +pub mod export { + pub use libc::{c_char, c_int}; + pub use openssl_sys::{ + init, ERR_get_next_error_library, ERR_load_strings, ERR_PACK, ERR_STRING_DATA, + }; + pub use std::borrow::Cow; + pub use std::option::Option; + pub use std::ptr::null; + pub use std::sync::Once; +} + +pub trait Library { + fn id() -> c_int; +} + +pub struct Function(c_int, PhantomData); + +impl Function { + #[inline] + pub const fn from_raw(raw: c_int) -> Function { + Function(raw, PhantomData) + } + + #[inline] + pub const fn as_raw(&self) -> c_int { + self.0 + } +} + +pub struct Reason(c_int, PhantomData); + +impl Reason { + #[inline] + pub const fn from_raw(raw: c_int) -> Reason { + Reason(raw, PhantomData) + } + + #[inline] + pub const fn as_raw(&self) -> c_int { + self.0 + } +} + +/// This is not considered part of this crate's public API. It is subject to change at any time. +/// +/// # Safety +/// +/// `file` and `message` must be null-terminated. +#[doc(hidden)] +pub unsafe fn __put_error( + func: Function, + reason: Reason, + file: &'static str, + line: u32, + message: Option>, +) where + T: Library, +{ + openssl_sys::ERR_put_error( + T::id(), + func.as_raw(), + reason.as_raw(), + file.as_ptr() as *const c_char, + line as c_int, + ); + let data = match message { + Some(Cow::Borrowed(s)) => Some((s.as_ptr() as *const c_char as *mut c_char, 0)), + Some(Cow::Owned(s)) => { + let ptr = openssl_sys::CRYPTO_malloc( + s.len() as _, + concat!(file!(), "\0").as_ptr() as *const c_char, + line!() as c_int, + ) as *mut c_char; + if ptr.is_null() { + None + } else { + ptr::copy_nonoverlapping(s.as_ptr(), ptr as *mut u8, s.len()); + Some((ptr, openssl_sys::ERR_TXT_MALLOCED)) + } + } + None => None + }; + if let Some((ptr, flags)) = data { + openssl_sys::ERR_set_error_data(ptr, flags | openssl_sys::ERR_TXT_STRING); + } +} + +#[macro_export] +macro_rules! put_error { + ($function:expr, $reason:expr) => { + unsafe { + $crate::__put_error( + $function, + $reason, + concat!(file!(), "\0"), + line!(), + $crate::export::Option::None, + ); + } + }; + ($function:expr, $reason:expr, $message:expr) => { + unsafe { + $crate::__put_error( + $function, + $reason, + concat!(file!(), "\0"), + line!(), + $crate::export::Option::Some($crate::export::Cow::Borrowed( + concat!($message, "\0"), + )), + ); + } + }; + ($function:expr, $reason:expr, $message:expr, $($args:tt)*) => { + unsafe { + $crate::__put_error( + $function, + $reason, + concat!(file!(), "\0"), + line!(), + $crate::export::Option::Some($crate::export::Cow::Owned( + format!(concat!($message, "\0"), $($args)*)), + ), + ); + } + }; +} + +#[macro_export] +macro_rules! openssl_errors { + ($( + $lib_vis:vis library $lib_name:ident($lib_str:expr) { + functions { + $( + $func_name:ident($func_str:expr); + )* + } + + reasons { + $( + $reason_name:ident($reason_str:expr); + )* + } + } + )*) => {$( + $lib_vis enum $lib_name {} + + impl $crate::Library for $lib_name { + fn id() -> $crate::export::c_int { + static INIT: $crate::export::Once = $crate::export::Once::new(); + static mut LIB_NUM: $crate::export::c_int = 0; + static mut STRINGS: [$crate::export::ERR_STRING_DATA; 2 + $crate::openssl_errors!(@count $($func_name;)* $($reason_name;)*)] = [ + $crate::export::ERR_STRING_DATA { + error: 0, + string: concat!($lib_str, "\0").as_ptr() as *const $crate::export::c_char, + }, + $( + $crate::export::ERR_STRING_DATA { + error: $crate::export::ERR_PACK(0, $lib_name::$func_name.as_raw(), 0), + string: concat!($func_str, "\0").as_ptr() as *const $crate::export::c_char, + }, + )* + $( + $crate::export::ERR_STRING_DATA { + error: $crate::export::ERR_PACK(0, 0, $lib_name::$reason_name.as_raw()), + string: concat!($reason_str, "\0").as_ptr() as *const $crate::export::c_char, + }, + )* + $crate::export::ERR_STRING_DATA { + error: 0, + string: $crate::export::null(), + } + ]; + + unsafe { + INIT.call_once(|| { + $crate::export::init(); + LIB_NUM = $crate::export::ERR_get_next_error_library(); + STRINGS[0].error = $crate::export::ERR_PACK(LIB_NUM, 0, 0); + $crate::export::ERR_load_strings(LIB_NUM, STRINGS.as_mut_ptr()); + }); + + LIB_NUM + } + } + } + + impl $lib_name { + $crate::openssl_errors!(@func_consts $lib_name; 1; $($func_name;)*); + $crate::openssl_errors!(@reason_consts $lib_name; 1; $($reason_name;)*); + } + )*}; + (@func_consts $lib_name:ident; $n:expr; $name:ident; $($tt:tt)*) => { + pub const $name: $crate::Function<$lib_name> = $crate::Function::from_raw($n); + $crate::openssl_errors!(@func_consts $lib_name; $n + 1; $($tt)*); + }; + (@func_consts $lib_name:ident; $n:expr;) => {}; + (@reason_consts $lib_name:ident; $n:expr; $name:ident; $($tt:tt)*) => { + pub const $name: $crate::Reason<$lib_name> = $crate::Reason::from_raw($n); + $crate::openssl_errors!(@reason_consts $lib_name; $n + 1; $($tt)*); + }; + (@reason_consts $lib_name:ident; $n:expr;) => {}; + (@count $i:ident; $($tt:tt)*) => { + 1 + $crate::openssl_errors!(@count $($tt)*) + }; + (@count) => { 0 }; +} diff --git a/openssl-errors/tests/test.rs b/openssl-errors/tests/test.rs new file mode 100644 index 00000000..86dfc3b1 --- /dev/null +++ b/openssl-errors/tests/test.rs @@ -0,0 +1,54 @@ +use openssl::error::Error; + +openssl_errors::openssl_errors! { + library Test("test library") { + functions { + FOO("function foo"); + BAR("function bar"); + } + + reasons { + NO_MILK("out of milk"); + NO_BACON("out of bacon"); + } + } +} + +#[test] +fn basic() { + openssl_errors::put_error!(Test::FOO, Test::NO_MILK); + + let error = Error::get().unwrap(); + assert_eq!(error.library().unwrap(), "test library"); + assert_eq!(error.function().unwrap(), "function foo"); + assert_eq!(error.reason().unwrap(), "out of milk"); + assert_eq!(error.file(), "openssl-errors/tests/test.rs"); + assert_eq!(error.line(), 19); + assert_eq!(error.data(), None); +} + +#[test] +fn static_data() { + openssl_errors::put_error!(Test::BAR, Test::NO_BACON, "foobar"); + + let error = Error::get().unwrap(); + assert_eq!(error.library().unwrap(), "test library"); + assert_eq!(error.function().unwrap(), "function bar"); + assert_eq!(error.reason().unwrap(), "out of bacon"); + assert_eq!(error.file(), "openssl-errors/tests/test.rs"); + assert_eq!(error.line(), 32); + assert_eq!(error.data(), Some("foobar")); +} + +#[test] +fn dynamic_data() { + openssl_errors::put_error!(Test::BAR, Test::NO_MILK, "hello {}", "world"); + + let error = Error::get().unwrap(); + assert_eq!(error.library().unwrap(), "test library"); + assert_eq!(error.function().unwrap(), "function bar"); + assert_eq!(error.reason().unwrap(), "out of milk"); + assert_eq!(error.file(), "openssl-errors/tests/test.rs"); + assert_eq!(error.line(), 45); + assert_eq!(error.data(), Some("hello world")); +}