use crate::types::{
ErrorKind, FromRedisValue, RedisError, RedisResult, RedisWrite, ToRedisArgs, Value,
};
macro_rules! not_convertible_error {
($v:expr, $det:expr) => {
RedisError::from((
ErrorKind::TypeError,
"Response type not convertible",
format!("{:?} (response was {:?})", $det, $v),
))
};
}
#[derive(Debug, Eq, PartialEq)]
pub enum Rule {
On,
Off,
AddCommand(String),
RemoveCommand(String),
AddCategory(String),
RemoveCategory(String),
AllCommands,
NoCommands,
AddPass(String),
RemovePass(String),
AddHashedPass(String),
RemoveHashedPass(String),
NoPass,
ResetPass,
Pattern(String),
AllKeys,
ResetKeys,
Reset,
}
impl ToRedisArgs for Rule {
fn write_redis_args<W>(&self, out: &mut W)
where
W: ?Sized + RedisWrite,
{
use self::Rule::*;
match self {
On => out.write_arg(b"on"),
Off => out.write_arg(b"off"),
AddCommand(cmd) => out.write_arg_fmt(format_args!("+{}", cmd)),
RemoveCommand(cmd) => out.write_arg_fmt(format_args!("-{}", cmd)),
AddCategory(cat) => out.write_arg_fmt(format_args!("+@{}", cat)),
RemoveCategory(cat) => out.write_arg_fmt(format_args!("-@{}", cat)),
AllCommands => out.write_arg(b"allcommands"),
NoCommands => out.write_arg(b"nocommands"),
AddPass(pass) => out.write_arg_fmt(format_args!(">{}", pass)),
RemovePass(pass) => out.write_arg_fmt(format_args!("<{}", pass)),
AddHashedPass(pass) => out.write_arg_fmt(format_args!("#{}", pass)),
RemoveHashedPass(pass) => out.write_arg_fmt(format_args!("!{}", pass)),
NoPass => out.write_arg(b"nopass"),
ResetPass => out.write_arg(b"resetpass"),
Pattern(pat) => out.write_arg_fmt(format_args!("~{}", pat)),
AllKeys => out.write_arg(b"allkeys"),
ResetKeys => out.write_arg(b"resetkeys"),
Reset => out.write_arg(b"reset"),
};
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct AclInfo {
pub flags: Vec<Rule>,
pub passwords: Vec<Rule>,
pub commands: Vec<Rule>,
pub keys: Vec<Rule>,
}
impl FromRedisValue for AclInfo {
fn from_redis_value(v: &Value) -> RedisResult<Self> {
let mut it = v
.as_sequence()
.ok_or_else(|| not_convertible_error!(v, ""))?
.iter()
.skip(1)
.step_by(2);
let (flags, passwords, commands, keys) = match (it.next(), it.next(), it.next(), it.next())
{
(Some(flags), Some(passwords), Some(commands), Some(keys)) => {
let flags = flags
.as_sequence()
.ok_or_else(|| {
not_convertible_error!(flags, "Expect a bulk response of ACL flags")
})?
.iter()
.map(|flag| match flag {
Value::Data(flag) => match flag.as_slice() {
b"on" => Ok(Rule::On),
b"off" => Ok(Rule::Off),
b"allkeys" => Ok(Rule::AllKeys),
b"allcommands" => Ok(Rule::AllCommands),
b"nopass" => Ok(Rule::NoPass),
_ => Err(not_convertible_error!(flag, "Expect a valid ACL flag")),
},
_ => Err(not_convertible_error!(
flag,
"Expect an arbitrary binary data"
)),
})
.collect::<RedisResult<_>>()?;
let passwords = passwords
.as_sequence()
.ok_or_else(|| {
not_convertible_error!(flags, "Expect a bulk response of ACL flags")
})?
.iter()
.map(|pass| Ok(Rule::AddHashedPass(String::from_redis_value(pass)?)))
.collect::<RedisResult<_>>()?;
let commands = match commands {
Value::Data(cmd) => std::str::from_utf8(cmd)?,
_ => {
return Err(not_convertible_error!(
commands,
"Expect a valid UTF8 string"
))
}
}
.split_terminator(' ')
.map(|cmd| match cmd {
x if x.starts_with("+@") => Ok(Rule::AddCategory(x[2..].to_owned())),
x if x.starts_with("-@") => Ok(Rule::RemoveCategory(x[2..].to_owned())),
x if x.starts_with('+') => Ok(Rule::AddCommand(x[1..].to_owned())),
x if x.starts_with('-') => Ok(Rule::RemoveCommand(x[1..].to_owned())),
_ => Err(not_convertible_error!(
cmd,
"Expect a command addition/removal"
)),
})
.collect::<RedisResult<_>>()?;
let keys = keys
.as_sequence()
.ok_or_else(|| not_convertible_error!(keys, ""))?
.iter()
.map(|pat| Ok(Rule::Pattern(String::from_redis_value(pat)?)))
.collect::<RedisResult<_>>()?;
(flags, passwords, commands, keys)
}
_ => {
return Err(not_convertible_error!(
v,
"Expect a resposne from `ACL GETUSER`"
))
}
};
Ok(Self {
flags,
passwords,
commands,
keys,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! assert_args {
($rule:expr, $arg:expr) => {
assert_eq!($rule.to_redis_args(), vec![$arg.to_vec()]);
};
}
#[test]
fn test_rule_to_arg() {
use self::Rule::*;
assert_args!(On, b"on");
assert_args!(Off, b"off");
assert_args!(AddCommand("set".to_owned()), b"+set");
assert_args!(RemoveCommand("set".to_owned()), b"-set");
assert_args!(AddCategory("hyperloglog".to_owned()), b"+@hyperloglog");
assert_args!(RemoveCategory("hyperloglog".to_owned()), b"-@hyperloglog");
assert_args!(AllCommands, b"allcommands");
assert_args!(NoCommands, b"nocommands");
assert_args!(AddPass("mypass".to_owned()), b">mypass");
assert_args!(RemovePass("mypass".to_owned()), b"<mypass");
assert_args!(
AddHashedPass(
"c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2".to_owned()
),
b"#c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"
);
assert_args!(
RemoveHashedPass(
"c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2".to_owned()
),
b"!c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"
);
assert_args!(NoPass, b"nopass");
assert_args!(Pattern("pat:*".to_owned()), b"~pat:*");
assert_args!(AllKeys, b"allkeys");
assert_args!(ResetKeys, b"resetkeys");
assert_args!(Reset, b"reset");
}
#[test]
fn test_from_redis_value() {
let redis_value = Value::Bulk(vec![
Value::Data("flags".into()),
Value::Bulk(vec![Value::Data("on".into())]),
Value::Data("passwords".into()),
Value::Bulk(vec![]),
Value::Data("commands".into()),
Value::Data("-@all +get".into()),
Value::Data("keys".into()),
Value::Bulk(vec![Value::Data("pat:*".into())]),
]);
let acl_info = AclInfo::from_redis_value(&redis_value).expect("Parse successfully");
assert_eq!(
acl_info,
AclInfo {
flags: vec![Rule::On],
passwords: vec![],
commands: vec![
Rule::RemoveCategory("all".to_owned()),
Rule::AddCommand("get".to_owned()),
],
keys: vec![Rule::Pattern("pat:*".to_owned())],
}
);
}
}