use super::args::{self, Args, is};
use super::lua::{self, library};
use super::table::Spec;
use super::{Server, Session};
use crate::reply::Out;
use yo_common::{Code, Error, Result};
use yo_kv::rdb;
pub(super) fn execute(
server: &Server,
session: &mut Session,
spec: &Spec,
args: Args<'_>,
out: &mut Out,
) -> Result<()> {
match spec.name {
"eval" | "eval_ro" => eval(server, session, spec.name.ends_with("_ro"), args, out),
"evalsha" | "evalsha_ro" => evalsha(server, session, spec.name.ends_with("_ro"), args, out),
"fcall" | "fcall_ro" => fcall(server, session, spec.name.ends_with("_ro"), args, out),
"script" => script(server, args, out),
"function" => function(server, args, out),
other => unreachable!("scripting command with no body: {other}"),
}
}
fn eval(
server: &Server,
session: &mut Session,
ro: bool,
args: Args<'_>,
out: &mut Out,
) -> Result<()> {
let body = args.get(1).to_vec();
let split = numkeys(&args)?;
let sha = yo_alloc::allow(|| server.scripts.lock().add(&body));
let found = Found {
body: &body,
sha: &sha,
ro,
split,
};
run(server, session, &found, args, out);
Ok(())
}
fn evalsha(
server: &Server,
session: &mut Session,
ro: bool,
args: Args<'_>,
out: &mut Out,
) -> Result<()> {
let split = numkeys(&args)?;
let Some(sha) = digest(args.get(1)) else {
no_script(out);
return Ok(());
};
let Some(body) = yo_alloc::allow(|| server.scripts.lock().body(&sha)) else {
no_script(out);
return Ok(());
};
let found = Found {
body: &body,
sha: &sha,
ro,
split,
};
run(server, session, &found, args, out);
Ok(())
}
struct Found<'a> {
body: &'a [u8],
sha: &'a [u8; 40],
ro: bool,
split: usize,
}
fn run(server: &Server, session: &mut Session, found: &Found<'_>, args: Args<'_>, out: &mut Out) {
yo_alloc::allow(|| {
let words: Vec<&[u8]> = (3..args.len()).map(|i| args.get(i)).collect();
let (keys, argv) = words.split_at(found.split);
let ask = lua::Ask {
keys,
argv,
name: found.sha,
ro: found.ro,
};
lua::run(server, session, found.body, &ask, out);
});
}
fn numkeys(args: &Args<'_>) -> Result<usize> {
let n = args.int(2)?;
if n < 0 {
return Err(Error::new(
Code::Invalid,
"Number of keys can't be negative",
));
}
let usable = i64::try_from(args.len() - 3).unwrap_or(i64::MAX);
if n > usable {
return Err(Error::new(
Code::Invalid,
"Number of keys can't be greater than number of args",
));
}
Ok(usize::try_from(n).unwrap_or(0))
}
fn digest(arg: &[u8]) -> Option<[u8; 40]> {
let mut sha = [0u8; 40];
if arg.len() != sha.len() {
return None;
}
for (slot, &b) in sha.iter_mut().zip(arg) {
if !b.is_ascii_hexdigit() {
return None;
}
*slot = b.to_ascii_lowercase();
}
Some(sha)
}
fn no_script(out: &mut Out) {
out.error_line(b"NOSCRIPT ", b"No matching script. Please use EVAL.");
}
fn script(server: &Server, args: Args<'_>, out: &mut Out) -> Result<()> {
let sub = args.get(1);
if is(sub, b"FLUSH") {
if args.len() > 3 || (args.len() == 3 && !mode(args.get(2))) {
return Err(Error::new(
Code::Invalid,
"SCRIPT FLUSH only support SYNC|ASYNC option",
));
}
yo_alloc::allow(|| server.scripts.lock().wipe());
out.ok();
} else if is(sub, b"LOAD") {
if args.len() != 3 {
return Err(args::wrong_arity_sub("script", "load"));
}
let body = args.get(2).to_vec();
if let Err(why) = lua::compiles(&body) {
return Err(yo_alloc::allow(|| {
Error::fmt(
Code::Invalid,
format_args!("Error compiling script (new function): {why}"),
)
}));
}
let sha = yo_alloc::allow(|| server.scripts.lock().add(&body));
out.bulk(&sha);
} else if is(sub, b"EXISTS") {
if args.len() < 3 {
return Err(args::wrong_arity_sub("script", "exists"));
}
let held = server.scripts.lock();
out.array(args.len() - 2);
for i in 2..args.len() {
let there = digest(args.get(i)).is_some_and(|sha| held.has(&sha));
out.int(i64::from(there));
}
} else if is(sub, b"KILL") {
if args.len() != 2 {
return Err(args::wrong_arity_sub("script", "kill"));
}
out.error_line(b"NOTBUSY ", b"No scripts in execution right now.");
} else if is(sub, b"DEBUG") {
if args.len() != 3 {
return Err(args::wrong_arity_sub("script", "debug"));
}
if !is(args.get(2), b"YES") && !is(args.get(2), b"SYNC") && !is(args.get(2), b"NO") {
return Err(Error::new(Code::Invalid, "Use SCRIPT DEBUG YES/SYNC/NO"));
}
out.ok();
} else if is(sub, b"HELP") {
if args.len() != 2 {
return Err(args::wrong_arity_sub("script", "help"));
}
super::server::help(out, SCRIPT_HELP);
} else {
return Err(args::unknown_subcommand(sub, "SCRIPT"));
}
Ok(())
}
fn fcall(
server: &Server,
session: &mut Session,
ro: bool,
args: Args<'_>,
out: &mut Out,
) -> Result<()> {
let found = yo_alloc::allow(|| {
let held = server.libraries.lock();
held.function(args.get(1)).map(|(lib, f)| Taken {
library: lib.name.to_string(),
function: f.name.to_string(),
sha: lib.sha,
body: lib.code[lib.at..].to_vec(),
no_writes: f.flags & library::NO_WRITES != 0,
})
});
let Some(found) = found else {
return Err(Error::new(Code::Unsupported, "Function not found"));
};
let Ok(n) = args.int(2) else {
return Err(Error::new(Code::Invalid, "Bad number of keys provided"));
};
let usable = i64::try_from(args.len() - 3).unwrap_or(i64::MAX);
if n > usable {
return Err(Error::new(
Code::Invalid,
"Number of keys can't be greater than number of args",
));
}
if n < 0 {
return Err(Error::new(
Code::Invalid,
"Number of keys can't be negative",
));
}
if ro && !found.no_writes {
return Err(Error::new(
Code::Unsupported,
"Can not execute a script with write flag using *_ro command.",
));
}
yo_alloc::allow(|| {
let words: Vec<&[u8]> = (3..args.len()).map(|i| args.get(i)).collect();
let (keys, argv) = words.split_at(usize::try_from(n).unwrap_or(0));
let call = lua::Call {
library: &found.library,
sha: &found.sha,
body: &found.body,
function: &found.function,
};
let ask = lua::Ask {
keys,
argv,
name: found.function.as_bytes(),
ro: ro || found.no_writes,
};
lua::fcall(server, session, &call, &ask, out);
});
Ok(())
}
struct Taken {
library: String,
function: String,
sha: [u8; 40],
body: Vec<u8>,
no_writes: bool,
}
fn function(server: &Server, args: Args<'_>, out: &mut Out) -> Result<()> {
let sub = args.get(1);
if is(sub, b"LOAD") {
return load(server, args, out);
} else if is(sub, b"LIST") {
return list(server, args, out);
} else if is(sub, b"STATS") {
if args.len() != 2 {
return Err(args::wrong_arity_sub("function", "stats"));
}
let (libraries, functions) = server.libraries.lock().counts();
out.map(2);
out.bulk(b"running_script");
out.nil();
out.bulk(b"engines");
out.map(1);
out.bulk(library::ENGINE.as_bytes());
out.map(2);
out.bulk(b"libraries_count");
out.int(i64::try_from(libraries).unwrap_or(i64::MAX));
out.bulk(b"functions_count");
out.int(i64::try_from(functions).unwrap_or(i64::MAX));
return Ok(());
} else if is(sub, b"KILL") {
if args.len() != 2 {
return Err(args::wrong_arity_sub("function", "kill"));
}
out.error_line(b"NOTBUSY ", b"No scripts in execution right now.");
return Ok(());
} else if is(sub, b"FLUSH") {
if args.len() > 3 {
return Err(unknown_or_arity(sub));
}
if args.len() == 3 && !mode(args.get(2)) {
return Err(Error::new(
Code::Invalid,
"FUNCTION FLUSH only supports SYNC|ASYNC option",
));
}
yo_alloc::allow(|| server.libraries.lock().wipe());
out.ok();
} else if is(sub, b"DELETE") {
if args.len() != 3 {
return Err(args::wrong_arity_sub("function", "delete"));
}
if !yo_alloc::allow(|| server.libraries.lock().remove(args.get(2))) {
return Err(Error::new(Code::Unsupported, "Library not found"));
}
out.ok();
} else if is(sub, b"DUMP") {
return dump(server, args, out);
} else if is(sub, b"RESTORE") {
return restore(server, args, out);
} else if is(sub, b"HELP") {
if args.len() != 2 {
return Err(args::wrong_arity_sub("function", "help"));
}
super::server::help(out, FUNCTION_HELP);
} else {
return Err(args::unknown_subcommand(sub, "FUNCTION"));
}
Ok(())
}
fn load(server: &Server, args: Args<'_>, out: &mut Out) -> Result<()> {
if args.len() < 3 {
return Err(args::wrong_arity_sub("function", "load"));
}
let mut replace = false;
for i in 2..args.len() - 1 {
if is(args.get(i), b"REPLACE") {
replace = true;
} else {
return Err(yo_alloc::allow(|| {
Error::fmt(
Code::Invalid,
format_args!(
"Unknown option given: {}",
String::from_utf8_lossy(args.get(i))
),
)
}));
}
}
let code = args.get(args.len() - 1);
yo_alloc::allow(|| {
let mut held = server.libraries.lock();
let name = take(&mut held, code, replace)?;
drop(held);
out.bulk(name.as_bytes());
Ok(())
})
}
fn take(held: &mut library::Libraries, code: &[u8], replace: bool) -> Result<String> {
let meta = library::metadata(code)?;
if !library::named(&meta.name) {
return Err(library::bad_name());
}
if !meta.engine.eq_ignore_ascii_case(library::ENGINE.as_bytes()) {
return Err(Error::fmt(
Code::Unsupported,
format_args!(
"Engine '{}' not found",
String::from_utf8_lossy(&meta.engine)
),
));
}
let name = String::from_utf8_lossy(&meta.name).into_owned();
let at = code.len() - meta.body.len();
let sha = lua::fingerprint(code);
if !replace && held.library(meta.name.as_slice()).is_some() {
return Err(Error::fmt(
Code::Unsupported,
format_args!("Library '{name}' already exists"),
));
}
let funcs = match lua::install(&name, &sha, meta.body) {
Ok(funcs) => funcs,
Err(why) => return Err(Error::fmt(Code::Invalid, format_args!("{why}"))),
};
if funcs.is_empty() {
return Err(Error::new(Code::Invalid, "No functions registered"));
}
for f in &funcs {
if held.taken(&f.name, &name) {
return Err(Error::fmt(
Code::Unsupported,
format_args!("Function {} already exists", f.name),
));
}
}
held.insert(library::Library {
name: name.clone().into(),
code: code.to_vec().into_boxed_slice(),
at,
sha,
funcs,
});
Ok(name)
}
fn dump(server: &Server, args: Args<'_>, out: &mut Out) -> Result<()> {
if args.len() != 2 {
return Err(args::wrong_arity_sub("function", "dump"));
}
let payload = yo_alloc::allow(|| {
let held = server.libraries.lock();
rdb::functions(held.all().iter().map(|l| &*l.code))
});
out.bulk(&payload);
Ok(())
}
fn restore(server: &Server, args: Args<'_>, out: &mut Out) -> Result<()> {
if args.len() < 3 {
return Err(args::wrong_arity_sub("function", "restore"));
}
if args.len() > 4 {
return Err(unknown_or_arity(args.get(1)));
}
let mut policy = Policy::Append;
if args.len() == 4 {
let word = args.get(3);
policy = if is(word, b"APPEND") {
Policy::Append
} else if is(word, b"REPLACE") {
Policy::Replace
} else if is(word, b"FLUSH") {
Policy::Flush
} else {
return Err(Error::new(
Code::Invalid,
"Wrong restore policy given, value should be either FLUSH, APPEND or REPLACE.",
));
};
}
yo_alloc::allow(|| {
let codes = match rdb::libraries(args.get(2)) {
Ok(codes) => codes,
Err(why) => return Err(payload_error(why)),
};
let mut fresh = library::Libraries::default();
for code in &codes {
take(&mut fresh, code, false)?;
}
let mut held = server.libraries.lock();
match policy {
Policy::Flush => *held = fresh,
Policy::Append => held.join(fresh, false)?,
Policy::Replace => held.join(fresh, true)?,
}
drop(held);
out.ok();
Ok(())
})
}
#[derive(Clone, Copy)]
enum Policy {
Append,
Replace,
Flush,
}
fn payload_error(why: rdb::BadLibs) -> Error {
let said = match why {
rdb::BadLibs::Footer => "DUMP payload version or checksum are wrong",
rdb::BadLibs::PreGa => "Pre-GA function format not supported",
rdb::BadLibs::NotFunction => "given type is not a function",
rdb::BadLibs::Truncated => "Failed loading library payload",
};
Error::new(Code::Invalid, said)
}
fn list(server: &Server, args: Args<'_>, out: &mut Out) -> Result<()> {
let mut code = false;
let mut pattern: Option<&[u8]> = None;
let mut i = 2;
while i < args.len() {
let a = args.get(i);
if is(a, b"WITHCODE") && !code {
code = true;
i += 1;
} else if is(a, b"LIBRARYNAME") && pattern.is_none() {
if i + 1 >= args.len() {
return Err(Error::new(
Code::Invalid,
"library name argument was not given",
));
}
pattern = Some(args.get(i + 1));
i += 2;
} else {
return Err(yo_alloc::allow(|| {
Error::fmt(
Code::Invalid,
format_args!("Unknown argument {}", String::from_utf8_lossy(a)),
)
}));
}
}
let held = server.libraries.lock();
let shown = || {
held.all().iter().filter(|l| match pattern {
Some(p) => yo_common::glob::matches_nocase(p, l.name.as_bytes(), true),
None => true,
})
};
out.array(shown().count());
for lib in shown() {
out.map(if code { 4 } else { 3 });
out.bulk(b"library_name");
out.bulk(lib.name.as_bytes());
out.bulk(b"engine");
out.bulk(library::ENGINE.as_bytes());
out.bulk(b"functions");
out.array(lib.funcs.len());
for f in &lib.funcs {
out.map(3);
out.bulk(b"name");
out.bulk(f.name.as_bytes());
out.bulk(b"description");
match &f.desc {
Some(d) => out.bulk(d),
None => out.nil(),
}
out.bulk(b"flags");
let count = library::FLAGS
.iter()
.enumerate()
.filter(|(i, _)| f.flags & (1 << i) != 0)
.count();
out.set(count);
for (i, name) in library::FLAGS.iter().enumerate() {
if f.flags & (1 << i) != 0 {
out.simple(name.as_bytes());
}
}
}
if code {
out.bulk(b"library_code");
out.bulk(&lib.code);
}
}
Ok(())
}
fn mode(arg: &[u8]) -> bool {
is(arg, b"SYNC") || is(arg, b"ASYNC")
}
fn unknown_or_arity(sub: &[u8]) -> Error {
yo_alloc::allow(|| {
Error::fmt(
Code::Unsupported,
format_args!(
"unknown subcommand or wrong number of arguments for '{}'. Try FUNCTION HELP.",
String::from_utf8_lossy(sub)
),
)
})
}
const SCRIPT_HELP: &[&str] = &[
"SCRIPT <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
"DEBUG (YES|SYNC|NO)",
" Set the debug mode for subsequent scripts executed.",
"EXISTS <sha1> [<sha1> ...]",
" Return information about the existence of the scripts in the script cache.",
"FLUSH [ASYNC|SYNC]",
" Flush the Lua scripts cache. Very dangerous on replicas.",
" When called without the optional mode argument, the behavior is determined by the",
" lazyfree-lazy-user-flush configuration directive. Valid modes are:",
" * ASYNC: Asynchronously flush the scripts cache.",
" * SYNC: Synchronously flush the scripts cache.",
"KILL",
" Kill the currently executing Lua script.",
"LOAD <script>",
" Load a script into the scripts cache without executing it.",
"HELP",
" Print this help.",
];
const FUNCTION_HELP: &[&str] = &[
"FUNCTION <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
"LOAD [REPLACE] <FUNCTION CODE>",
" Create a new library with the given library name and code.",
"DELETE <LIBRARY NAME>",
" Delete the given library.",
"LIST [LIBRARYNAME PATTERN] [WITHCODE]",
" Return general information on all the libraries:",
" * Library name",
" * The engine used to run the Library",
" * Functions list",
" * Library code (if WITHCODE is given)",
" It also possible to get only function that matches a pattern using LIBRARYNAME argument.",
"STATS",
" Return information about the current function running:",
" * Function name",
" * Command used to run the function",
" * Duration in MS that the function is running",
" If no function is running, return nil",
" In addition, returns a list of available engines.",
"KILL",
" Kill the current running function.",
"FLUSH [ASYNC|SYNC]",
" Delete all the libraries.",
" When called without the optional mode argument, the behavior is determined by the",
" lazyfree-lazy-user-flush configuration directive. Valid modes are:",
" * ASYNC: Asynchronously flush the libraries.",
" * SYNC: Synchronously flush the libraries.",
"DUMP",
" Return a serialized payload representing the current libraries, can be restored using FUNCTION RESTORE command",
"RESTORE <PAYLOAD> [FLUSH|APPEND|REPLACE]",
" Restore the libraries represented by the given payload, it is possible to give a restore policy to",
" control how to handle existing libraries (default APPEND):",
" * FLUSH: delete all existing libraries.",
" * APPEND: appends the restored libraries to the existing libraries. On collision, abort.",
" * REPLACE: appends the restored libraries to the existing libraries, On collision, replace the old",
" libraries with the new libraries (notice that even on this option there is a chance of failure",
" in case of functions name collision with another library).",
"HELP",
" Print this help.",
];