//! Searches paths for the plugin name to find it's shared object file
use super ::* ;
use std ::path ::{
PathBuf ,
Path ,
} ;
use std ::{
fs , io ,
borrow ::{
Cow , Borrow ,
} ,
//collections::HashMap,
} ;
use regex ::{
Regex , RegexBuilder ,
} ;
use lazy_static ::lazy_static ;
/// Specifies locations and associated rules of these locations for path lookups when searching for plugins.
pub mod locations {
use super ::* ;
use bitflags ::* ;
/// Describes a path an its trust level.
#[ derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Copy) ]
pub enum PathTrust < ' a >
{
Trusted ( & ' a str ) ,
Untrusted ( & ' a str ) ,
}
bitflags ! {
/// The rules a file in an unstructed lookup path must qualify for it to be considered for plugin candidacy.
pub struct UntrustedPathRules : u8
{
/// No restrictions on the file.
///
/// # Note
/// Must always be 0.
const UNRESTRICTED = 0 ;
/// Name ends in valid string from `LOOKUP_NAME_MATCHES_IN_UNTRUSTED`.
const NAME_MATCH = 1 < < 0 ;
/// Disallow symlinked files.
const NON_SYMLINK = 1 < < 1 ;
/// Disqualify executable-marked files.
const NON_EXEC = 1 < < 2 ;
/// Disqualify world-writable files.
const NON_WORLD_WRITABLE = 1 < < 3 ;
/// Only qualify root-owned files (can be unioned with other `_OWNED_ONLY` constants)
const ROOT_OWNED_ONLY = 1 < < 4 ;
/// Only qualify user-owned files (can be unioned with other `_OWNED_ONLY` constants)
const USER_OWNED_ONLY = 1 < < 5 ;
/// Owned by user or by root.
const USER_OR_ROOT_OWNED = Self ::ROOT_OWNED_ONLY . bits | Self ::USER_OWNED_ONLY . bits ;
/// Disqualify files that are accessible at all by anyone other than the current user (or root, which can accesss everything.)
const USER_ACCESSIBLE_ONLY = 1 < < 6 ;
/// Disallow *all* untrusted files.
///
/// # Note
/// Must be highest value.
const INACCESSIBLE = 1 < < 7 ;
}
}
impl UntrustedPathRules
{
/// Contains function pointers for checking paths with every combination (**any**, not **all** in the mask) of rules in `UntrustedPathRules`.
///
/// Invalid functions will either:
/// * Panic if they are *never* supposed to be called in the pipeline (e.g `NAME_MATCH`.)
/// * Return false immediately.
pub const RULE_FUNCTION_CHECK_TABLE : [ & ' static ( dyn Fn ( & Path , & str ) -> bool + ' static ) ; 256 ] = Self ::generate_rule_table ( ) ;
/// Generate a table of 256 entries that which each single, or **OR**-combined mask entry of `UntrustedPathRules` has a corresponding path check function.
///
/// Invalid functions will either:
/// * Panic if they are *never* supposed to be called in the pipeline (e.g `NAME_MATCH`.)
/// * Return false immediately.
pub ( super ) const fn generate_rule_table ( ) -> [ & ' static ( dyn Fn ( & Path , & str ) -> bool + ' static ) ; 256 ]
{
let mut rule_table = Self ::generate_pruned_rule_table ( ) ;
//TODO: Add in combinations via ORing its single-bit entry functions.
rule_table
}
/// Generate a table of 256 entries that which each **single** (not combinations of) entry of `UntrustedPathRules` has a corresponding path check function.
#[ inline ]
const fn generate_pruned_rule_table ( ) -> [ & ' static ( dyn Fn ( & Path , & str ) -> bool + ' static ) ; 256 ]
{
type RuleCheckFunc < ' a > = ( dyn Fn ( & Path , & str ) -> bool + ' a ) ;
#[ inline ]
const fn disallow ( _ : & Path , _ : & str ) -> bool { false }
let mut table : [ & ' static RuleCheckFunc ; 256 ] = [ & ( disallow as fn ( & Path , & str ) -> bool ) ; 256 ] ;
macro_rules! set {
( $func :expr ) = > {
{
let func : & ' static RuleCheckFunc < ' static > = & ( $func ) ;
func
}
} ;
( $name :ident = > $func :expr ) = > {
let bits = Self ::$name . bits as usize ;
// Fill in compositions based on previous entries into `table`.
let func : & ' static RuleCheckFunc < ' static > = set ! ( $func ) ;
/* let rv = {
let mut i : usize = 1 ;
//XXX: WHY doesn't this work???
while i < bits {
let prev = table [ i ] ;
table [ bits | i ] = set ! ( | path : & Path , name : & str | -> bool {
prev ( path , name ) & & func ( path , name )
} ) ;
i + = 1 ;
} * /
table [ bits ] = func
//};
//rv
} ;
( $name :ident as ! $panic_msg :literal ) = > {
#[ inline(never) ]
#[ cold ]
fn _panic_with_msg ( ) -> ! {
panic! ( $panic_msg )
}
set ! ( $name = > | _ , _ | _panic_with_msg ( ) )
} ;
/* FUCK THIS, I HATE ITERATON IN MACROS< WHY DOES IT HAVE TO BE SO RECURSIVELY LEAKILY RETARDED AAAA(@ compose ($f_name:ident, $f_file:ident) via $operator:tt $ignore:tt; $first:ident, $second:ident $($rest:tt) * )
= > ( table [ Self ::$first . bits ( ) as usize ] ( $f_name , $f_file ) $operator set ! ( @ compose ( $f_name , $f_file ) via $operator & & ; Self ::$second . bits ( ) , $( $rest ) * ) ) ;
( @ compose ( $f_name :ident , $f_file :ident ) $( via $operator :tt ) ? ; $( $first :ident ) ? $(, ) ? )
= > ( $( table [ Self ::$first . bits ( ) as usize ] ) ? ) ;
( $name :ident $( ; use $operator :tt ) ? = > $( $other :ident ) , + ) = > {
set ! ( $name = > | path , filename | {
set ! ( @ compose ( path , filename ) via $( $operator ) ? & & ; $( $other ) , + )
} ) ;
} * /
}
/// In lookups, use process EUID instead of UID.
const USE_EFFECTIVE_UID : bool = true ;
/// Get UID or **E**UID.
#[ inline(always) ]
fn get_uid < const E : bool > ( ) -> u32
{
extern "C" {
fn getuid ( ) -> u32 ; // uid_t
//XXX: Should we use process's UID or EUID? It seems we should use EUID.
fn geteuid ( ) -> u32 ; // uid_t
}
// SAFETY: These are effectively pure.
unsafe {
if E {
geteuid ( )
} else {
getuid ( )
}
}
}
#[ inline(always) ]
fn check_uid < const CACHED : bool > ( against : impl PartialEq < u32 > ) -> bool
{
//TODO: Allow these to be reset, somehow? Idk... Make them regular static atomics?
lazy_static ! {
static ref USER_UID_CACHED : u32 = get_uid ::< false > ( ) ;
static ref USER_EUID_CACHED : u32 = get_uid ::< true > ( ) ;
}
against = = if CACHED {
if USE_EFFECTIVE_UID {
* USER_EUID_CACHED
} else {
* USER_UID_CACHED
}
} else {
get_uid ::< USE_EFFECTIVE_UID > ( )
}
}
set ! ( NAME_MATCH as ! "This should already have been checked" ) ;
set ! ( NON_SYMLINK = > | path : & Path , _ | ! path . is_symlink ( ) ) ;
set ! ( NON_EXEC = > | path , _ | {
use std ::os ::unix ::fs ::PermissionsExt as _ ;
use readable_perms ::{
User ,
Bit ,
} ;
const EXEC_BIT : u32 =
User ::Group . mode ( Bit ::Execute )
| User ::Other . mode ( Bit ::Execute )
| User ::Owner . mode ( Bit ::Execute ) ;
path . metadata ( )
. map ( | meta | meta . permissions ( ) . mode ( ) & EXEC_BIT = = 0 ) //TODO: XXX: Test this.
. unwrap_or ( false )
} ) ;
set ! ( NON_WORLD_WRITABLE = > | path , _ | {
use readable_perms ::{
PermissionsExt as _ ,
User ,
Bit ,
} ;
path . metadata ( )
. map ( | meta | ! meta . permissions ( ) . unix ( ) . has_mask ( User ::Other , Bit ::Write ) )
. unwrap_or ( false )
} ) ;
set ! ( ROOT_OWNED_ONLY = > | path , _ | {
use std ::os ::unix ::fs ::MetadataExt as _ ;
path . metadata ( )
. map ( | meta | meta . uid ( ) = = 0 )
. unwrap_or ( false )
} ) ;
set ! ( USER_OWNED_ONLY = > | path , _ | {
use std ::os ::unix ::fs ::MetadataExt as _ ;
//let user_id = get_uid::<USE_EFFECTIVE_UID>();
path . metadata ( )
. map ( move | meta | check_uid ::< true > ( meta . uid ( ) ) )
. unwrap_or ( false )
} ) ;
set ! ( USER_ACCESSIBLE_ONLY = > | path , _ | {
//Only Owner should be able to read, write, or execute.
use std ::os ::unix ::fs ::PermissionsExt as _ ;
use readable_perms ::{
User ,
Bit ,
} ;
const BIT_ANY : Bit =
Bit ::Read
. union ( Bit ::Write )
. union ( Bit ::Execute ) ;
const OTHER_ACCESSORS : u32 =
User ::Group . mode ( BIT_ANY )
| User ::Other . mode ( BIT_ANY ) ;
path . metadata ( )
. map ( | meta | ! meta . permissions ( ) . mode ( ) & OTHER_ACCESSORS = = 0 ) //XXX: Test this
. unwrap_or ( false )
} ) ;
set ! ( USER_OR_ROOT_OWNED = > | path , _ | {
use std ::os ::unix ::fs ::MetadataExt as _ ;
//let user_id = get_uid::<USE_EFFECTIVE_UID>();
path . metadata ( )
. map ( move | meta | match meta . uid ( ) {
0 = > true ,
uid if check_uid ::< true > ( uid ) = > true ,
_ = > false
} )
. unwrap_or ( false )
} ) ;
//set!(USER_OR_ROOT_OWNED; use || => USER_OWNED_ONLY, ROOT_OWNED_ONLY);
//TODO: Composition of functions should be done via OR, how to insert all possible compositions into table? We get confusing error messages when we try inside `set!()`
table
}
}
impl Default for UntrustedPathRules
{
#[ inline ]
fn default ( ) -> Self
{
Self ::NAME_MATCH
}
}
impl < ' a > AsRef < str > for PathTrust < ' a >
{
#[ inline(always) ]
fn as_ref ( & self ) -> & str
{
match self {
Trusted ( a ) | Untrusted ( a ) = > a
}
}
}
impl < ' a > From < PathTrust < ' a > > for & ' a str
{
#[ inline ]
fn from ( from : PathTrust < ' a > ) -> Self
{
from . into_str ( )
}
}
impl < ' a > From < PathTrust < ' a > > for ( & ' a str , bool )
{
#[ inline ]
fn from ( from : PathTrust < ' a > ) -> Self
{
let trusted = from . is_trusted ( ) ;
( from . into_str ( ) , trusted )
}
}
// impl<'a> AsRef<Path> for PathTrust<'a>
// {
// #[inline]
// fn as_ref(&self) -> &Path
// {
// Path::new(AsRef::<str>::as_ref(self))
// }
// }
use PathTrust ::* ;
// The ordering of these variables is the order in which the plugin is looked-up in. The first match found is used.
/// Should paths be recursively travelled in lookups. (0 is for trusted paths, 1 is for untrusted)
pub static LOOKUP_PATH_RECURSION : ( bool , bool ) = ( true , true ) ;
/// Should symlinks be followed in a recursive lookup. (0 is for trusted paths, 1 is for untrusted)
pub static LOOKUP_PATH_FOLLOW_SYMLINKS : ( bool , bool ) = ( true , false ) ;
/// Trust plugin file lookups in non-contained `LOOKUP_PATH_HOME_BASE_NAME`s.
pub static LOOKUP_PATH_TRUST_HOME : bool = false ;
/// Base paths from user home to look-up plugin name.
pub static LOOKUP_PATH_HOME_BASE_NAME : & [ PathTrust ] = & [
Trusted ( ".rngcli/plugins/" ) ,
Untrusted ( ".rngcli/" ) . map_trust ( LOOKUP_PATH_TRUST_HOME ) ,
Untrusted ( ".config/rngcli/" ) . map_trust ( LOOKUP_PATH_TRUST_HOME ) ,
Untrusted ( ".config/" ) . map_trust ( LOOKUP_PATH_TRUST_HOME ) ,
] ;
/// Extra valid lookup paths
pub static LOOKUP_PATH_EXTRA_PATHS : & [ PathTrust ] = & [
Trusted ( "/usr/share/rngcli/" ) ,
Trusted ( "/usr/local/share/rngcli/" )
] ;
/// `PATH`-like env-vars to look up a plugin's name.
pub static LOOKUP_PATH_ENV_VARS : & [ PathTrust ] = & [
Trusted ( "RNGCLI_PATH" ) ,
Untrusted ( "PATH" ) ,
] ;
/// Allow lookup inside the current working directory.
pub static LOOKUP_PATH_ALLOW_CWD : bool = true ;
/// Files with these regex-matched names are considered in all lookup contexts (`Trusted` and `Untrusted`.)
pub static LOOKUP_NAME_MATCHES_IN_TRUSTED : & [ & ' static str ] = & [
r"\.rngp\.so$" ,
r"\.rngp$" ,
r"\.so$" ,
] ;
/// Files with these regex-matched names are considered in `Untrusted` lookup contexts (if specified by `LOOKUP_UNTRUSTED_PATH_RULES`)
pub static LOOKUP_NAME_MATCHES_IN_UNTRUSTED : & [ & ' static str ] = & [
r"\.rngp\.so$" ,
r"\.rngp$" ,
] ;
/// These rules must be met for lookups in untrusted paths.
pub static LOOKUP_UNTRUSTED_PATH_RULES : UntrustedPathRules =
UntrustedPathRules ::NAME_MATCH
. union ( UntrustedPathRules ::NON_SYMLINK )
. union ( UntrustedPathRules ::NON_WORLD_WRITABLE ) ;
impl < ' a > PathTrust < ' a >
{
#[ inline(always) ]
pub const fn into_str ( self ) -> & ' a str
{
match self {
Trusted ( a ) | Untrusted ( a ) = > a
}
}
#[ inline(always) ]
pub fn into_path ( self ) -> & ' a Path
{
match self {
Trusted ( a ) | Untrusted ( a ) = > Path ::new ( a )
}
}
#[ inline(always) ]
pub fn as_path ( & self ) -> & Path
{
Path ::new ( self . as_ref ( ) )
}
#[ inline(always) ]
pub const fn is_trusted ( self ) -> bool
{
if let Trusted ( _ ) = self {
true
} else {
false
}
}
#[ inline(always) ]
pub const fn is_untrusted ( self ) -> bool
{
! self . is_trusted ( )
}
/// Map the trust level of this `PathTrust`.
#[ inline(always) ]
pub const fn map_trust ( self , trust : bool ) -> Self
{
if trust { Trusted ( self . into_str ( ) ) }
else { Untrusted ( self . into_str ( ) ) }
}
}
}
#[ derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash) ]
struct LookupVisitor < P : AsRef < Path > > ( P ) ;
#[ derive(Debug) ]
struct PathLookup {
name : String ,
candidates : Vec < fs ::DirEntry > ,
trusted : bool ,
}
impl PathLookup
{
#[ inline ]
pub fn check_valid_path < const TRUSTED : bool > ( path : impl AsRef < Path > ) -> bool
{
#[ inline(always) ]
fn regex_builder ( pattern : impl AsRef < str > ) -> Regex {
RegexBuilder ::new ( pattern . as_ref ( ) )
. case_insensitive ( true )
. build ( ) . expect ( "Invalid regex in trusted pattern match array." )
}
/// Returns a function that returns `true` if any of the `matchers` match the input string.
#[ inline ]
const fn match_any < ' a , I : ' a > ( matchers : I ) -> impl Fn ( & str ) -> bool + ' a
where I : IntoIterator < Item = & ' a Regex > + Clone ,
{
move | s : & str | -> bool {
let matchers = matchers . clone ( ) . into_iter ( ) ;
for re in matchers {
if re . is_match ( s ) {
return true ;
}
}
false
}
}
/// Validate a file in an untrusted lookup.
#[ inline ]
fn validate_file_untrusted ( path : & Path , filename : & str ) -> bool
{
// By this point, `filename` has already matched successfully.
// Also Note: `filename` is just the basename of `path` as an `&str` (aliasing address-space.)
use locations ::UntrustedPathRules as Rule ;
use locations ::LOOKUP_UNTRUSTED_PATH_RULES as RULES ;
let mut bit = Rule ::INACCESSIBLE . bits ( ) ;
// No rules
if bit = = 0 /* `Rule::UNRESTRICTED` */ {
return true ;
}
macro_rules! check {
( $name :ident ) = > {
if RULES . contains ( Rule ::$name ) {
Rule ::RULE_FUNCTION_CHECK_TABLE [ Rule ::$name . bits ( ) as usize ] ( path , filename )
} else {
true
}
} ;
( try $( $op :tt ) ? $name :ident ) = > {
if $( $op ) ? check ! ( $name ) {
return false ;
}
}
}
// XXX: Ugh, for now, this shit must be checked independently.
check ! ( try ! USER_OR_ROOT_OWNED ) ;
// Check rest of rules in sequence. Hopefully this will be unrolled since the first `if` statement is all const-fn on a static constant.
while bit > 0
{
// SAFETY: We know `INACCESSIBLE` is the largest
if RULES . contains ( unsafe { Rule ::from_bits_unchecked ( bit ) } ) {
if ! Rule ::RULE_FUNCTION_CHECK_TABLE [ bit as usize ] ( path , filename ) {
return false ;
}
}
bit > > = 1 ;
}
true
}
lazy_static ! {
static ref TRUSTED_EXT_REGEX_MAP : Vec < Regex > =
locations ::LOOKUP_NAME_MATCHES_IN_TRUSTED . into_iter ( ) . map ( regex_builder ) . collect ( ) ;
static ref UNTRUSTED_EXT_REGEX_MAP : Vec < Regex > =
locations ::LOOKUP_NAME_MATCHES_IN_UNTRUSTED . into_iter ( ) . map ( regex_builder ) . collect ( ) ;
}
let path = path . as_ref ( ) ;
// Check if the path:
// * Exists
// * Is not a directory
// NOTE: We don't use `.is_file()` here, because we want to support symlinks and pseudofiles in trusted contexts, which `is_file()` reports as false.
// TODO: XXX: Maybe in the future allow directories to be plugins; they would contain a manifest, and each `.so` file in the directory will be loaded in order specified by the directory's manifest.
if ! path . exists ( ) | | path . is_dir ( ) {
return false ;
}
// Attempt to extract the filename, return false if it is invalid UTF8.
let filename = match path . file_name ( )
. and_then ( | os_fn | os_fn . to_str ( ) )
{
Some ( f ) = > f ,
_ = > return false ,
} ;
// Get the name matcher and rule checker for the file.
let ( matcher , checker ) = if TRUSTED {
// Trusted path lookup
( match_any ( TRUSTED_EXT_REGEX_MAP . iter ( ) ) , None ) //&NoChecker)
} else {
// Untrusted path lookup
let matcher = if locations ::LOOKUP_UNTRUSTED_PATH_RULES
. contains ( locations ::UntrustedPathRules ::NAME_MATCH )
{
// Name must match
match_any ( UNTRUSTED_EXT_REGEX_MAP . iter ( ) )
} else {
// Name needn't match
match_any ( TRUSTED_EXT_REGEX_MAP . iter ( ) )
} ;
( matcher , Some ( move | path | {
validate_file_untrusted ( path , filename )
} ) )
} ;
matcher ( filename ) // Check filename first
& & match checker {
Some ( untrusted ) = > untrusted ( path ) , // Check extra Untrusted path rules next
_ = > true
}
}
}
impl < P : AsRef < Path > > LookupVisitor < P >
{
pub fn start_lookup ( & self , trusted : bool , name : impl Into < String > ) -> io ::Result < PathLookup >
{
Ok ( PathLookup {
name : name . into ( ) ,
trusted ,
candidates : fs ::read_dir ( & self . 0 ) ? . collect ::< io ::Result < Vec < _ > > > ( ) ? ,
} )
}
}
/// Denoted a plugin and if it was loaded from a trusted path or not
#[ derive(Debug, Clone, Eq, Hash, PartialOrd, Ord) ]
pub enum PluginTrust < ' a >
{
Trusted ( Cow < ' a , Path > ) ,
Untrusted ( Cow < ' a , Path > )
}
impl < ' a > Borrow < Path > for PluginTrust < ' a >
{
#[ inline(always) ]
fn borrow ( & self ) -> & Path
{
match self {
Self ::Trusted ( a ) | Self ::Untrusted ( a ) = > a . borrow ( )
}
}
}
impl < ' a > AsRef < Path > for PluginTrust < ' a >
{
#[ inline ]
fn as_ref ( & self ) -> & Path
{
self . borrow ( )
}
}
impl < ' a > From < PluginTrust < ' a > > for Cow < ' a , Path >
{
#[ inline ]
fn from ( from : PluginTrust < ' a > ) -> Self
{
match from {
PluginTrust ::Trusted ( a ) | PluginTrust ::Untrusted ( a ) = > a
}
}
}
impl < ' a , T : AsRef < Path > + ? Sized > PartialEq < T > for PluginTrust < ' a >
{
#[ inline ]
fn eq ( & self , other : & T ) -> bool
{
other . as_ref ( ) = = self . as_ref ( )
}
}
impl < ' a > PluginTrust < ' a >
{
/// Was this plugin loaded from a trusted path?
#[ inline(always) ]
pub fn is_trusted ( & self ) -> bool
{
if let Self ::Trusted ( _ ) = self {
true
} else {
false
}
}
/// Was this plugin loaded from an untrusted path?
#[ inline ]
pub fn is_untrusted ( & self ) -> bool
{
! self . is_trusted ( )
}
}
impl From < PluginTrust < ' static > > for PathBuf
{
#[ inline ]
fn from ( from : PluginTrust < ' static > ) -> Self
{
match from {
PluginTrust ::Trusted ( a ) | PluginTrust ::Untrusted ( a ) = > a . into_owned ( )
}
}
}
pub fn lookup_plugin_name ( name : impl AsRef < str > ) -> impl IntoIterator < Item = PluginTrust < ' static > > + Send + ' static
{
todo! ( "Create a LookupVisitor(<path>) for each path in `locations`, then call `start_lookup(<trusted path>, name)` on each *in order* specified in `locations`" ) ;
None ::< PluginTrust > //TODO: ^ see above
}