diff --git a/.gitignore b/.gitignore index a40afb49..9a175c06 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,6 @@ snapstore.partmap *.deb .skytest_* *.pem -passphrase.txt \ No newline at end of file +passphrase.txt +*.db-tlog +*.db \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e72855d6..c084f43a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,17 @@ dependencies = [ "inout", ] +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -282,6 +293,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.3.3", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -315,6 +351,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "env_logger" version = "0.10.0" @@ -355,12 +397,33 @@ dependencies = [ "libc", ] +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "fd-lock" +version = "3.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys", +] + [[package]] name = "flate2" version = "1.0.26" @@ -482,6 +545,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + [[package]] name = "humantime" version = "2.1.0" @@ -632,6 +704,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", + "log", "wasi", "windows-sys", ] @@ -654,6 +727,26 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -838,6 +931,16 @@ dependencies = [ "scheduled-thread-pool", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.8.5" @@ -958,6 +1061,29 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustyline" +version = "12.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" +dependencies = [ + "bitflags 2.3.3", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "scopeguard", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + [[package]] name = "ryu" version = "1.0.15" @@ -1066,6 +1192,27 @@ dependencies = [ "digest", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1123,14 +1270,16 @@ dependencies = [ name = "skysh" version = "0.8.0" dependencies = [ + "crossterm", "libsky", + "rustyline", "skytable", ] [[package]] name = "skytable" version = "0.8.0" -source = "git+https://github.com/skytable/client-rust.git?branch=octave#b2b0ea7197d9a3425809ce269e30b74ddd3eb340" +source = "git+https://github.com/skytable/client-rust.git?branch=octave#ce16be88204044cd8358a5aa48521bd30e60ae33" dependencies = [ "async-trait", "bb8", @@ -1166,6 +1315,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + [[package]] name = "subtle" version = "2.5.0" @@ -1297,12 +1452,30 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "unsafe-libyaml" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.4.1" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 68312688..e27538bd 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -10,3 +10,5 @@ edition = "2021" # internal deps libsky = { path = "../libsky" } skytable = { git = "https://github.com/skytable/client-rust.git", branch = "octave" } +crossterm = "0.27.0" +rustyline = "12.0.0" diff --git a/cli/help_text/help b/cli/help_text/help new file mode 100644 index 00000000..8a4d6ab6 --- /dev/null +++ b/cli/help_text/help @@ -0,0 +1,30 @@ +skysh 0.8.0 +Sayan N. +The Skytable interactive shell (skysh) + +USAGE: + skysh [OPTIONS] + +FLAGS: + --help Diplays this help message + --version Displays the shell version + +OPTIONS: + --endpoint Set the endpoint for the connection + --user Set the user for this client session + --password Set the password for this client session + --tls-cert Set the TLS certificate to use (for TLS endpoints) + +NOTES: + - When no endpoint is specified, skysh will attempt to connect to the default + TCP endpoint `tcp@127.0.0.1:2003` + - When no user is specified, skysh will attempt to authenticate as root + - All connections need an username and password. If this is not provided + via arguments, it will be asked for interactively + - Endpoints are specified using the Skytable endpoint syntax. For example, + the default TCP endpoint is `tcp@127.0.0.1:2003` while the default TLS + endpoint is `tls@127.0.0.1:2004` + - If you choose to use a TLS endpoint, you must provide a certificate. + Failing to do so will throw an error, as expected + - All history is stored in the `.sky_history` file. If you wish to delete + it, simply remove the file \ No newline at end of file diff --git a/cli/help_text/welcome b/cli/help_text/welcome new file mode 100644 index 00000000..27c14bbd --- /dev/null +++ b/cli/help_text/welcome @@ -0,0 +1,16 @@ +Welcome to the Skytable Interactive shell (REPL environment). + +Here are a few tips to help you get started: +- Skytable uses its own query language called BlueQL. It is mostly like SQL +but with some important changes for security +- Since BlueQL doesn't need a query terminator ';' you do not need to use it +here for running queries +- You might be surprised to see that you can use literals in this REPL while +Skytable does not allow the use of literals for security concerns. This is +because whenever you run a query, the REPL turns it into a parameterized query. +- You can also run some `skysh` specific commands: + - `!help` displays this help message + - `clear` clears the terminal screen + - `exit` exits the REPL session + +Now, it's time to get querying! diff --git a/cli/src/args.rs b/cli/src/args.rs new file mode 100644 index 00000000..29923f8d --- /dev/null +++ b/cli/src/args.rs @@ -0,0 +1,221 @@ +/* + * Created on Wed Nov 15 2023 + * + * This file is a part of Skytable + * Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source + * NoSQL database written by Sayan Nandan ("the Author") with the + * vision to provide flexibility in data modelling without compromising + * on performance, queryability or scalability. + * + * Copyright (c) 2023, Sayan Nandan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * +*/ + +use { + crate::error::{CliError, CliResult}, + crossterm::{ + event::{self, Event, KeyCode, KeyEvent}, + terminal, + }, + std::{ + collections::{hash_map::Entry, HashMap}, + env, fs, + io::{self, Write}, + process::exit, + }, +}; + +const TXT_HELP: &str = include_str!("../help_text/help"); + +#[derive(Debug)] +pub struct ClientConfig { + pub kind: ClientConfigKind, + pub username: String, + pub password: String, +} + +impl ClientConfig { + pub fn new(kind: ClientConfigKind, username: String, password: String) -> Self { + Self { + kind, + username, + password, + } + } +} + +#[derive(Debug)] +pub enum ClientConfigKind { + Tcp(String, u16), + Tls(String, u16, String), +} + +#[derive(Debug)] +pub enum Task { + HelpMessage(String), + OpenShell(ClientConfig), +} + +enum TaskInner { + HelpMsg(String), + OpenShell(HashMap), +} + +fn load_env() -> CliResult { + let mut args = HashMap::new(); + let mut it = env::args().skip(1).into_iter(); + while let Some(arg) = it.next() { + let (arg, arg_val) = match arg.as_str() { + "--help" => return Ok(TaskInner::HelpMsg(TXT_HELP.into())), + "--version" => return Ok(TaskInner::HelpMsg(format!("skysh v{}", libsky::VERSION))), + _ if arg.starts_with("--") => match it.next() { + Some(arg_val) => (arg, arg_val), + None => { + // self contained? + let split: Vec<&str> = arg.split("=").collect(); + if split.len() != 2 { + return Err(CliError::ArgsErr(format!("expected value for {arg}"))); + } + (split[0].into(), split[1].into()) + } + }, + unknown_arg => { + return Err(CliError::ArgsErr(format!( + "unknown argument: {unknown_arg}" + ))) + } + }; + match args.entry(arg) { + Entry::Occupied(oe) => { + return Err(CliError::ArgsErr(format!( + "found duplicate values for {}", + oe.key() + ))) + } + Entry::Vacant(ve) => { + ve.insert(arg_val); + } + } + } + Ok(TaskInner::OpenShell(args)) +} + +pub fn parse() -> CliResult { + let mut args = match load_env()? { + TaskInner::HelpMsg(msg) => return Ok(Task::HelpMessage(msg)), + TaskInner::OpenShell(args) => args, + }; + let endpoint = match args.remove("--endpoint") { + None => ClientConfigKind::Tcp("127.0.0.1".into(), 2003), + Some(ep) => { + // should be in the format protocol@host:port + let proto_host_port: Vec<&str> = ep.split("@").collect(); + if proto_host_port.len() != 2 { + return Err(CliError::ArgsErr("invalid value for --endpoint".into())); + } + let (protocol, host_port) = (proto_host_port[0], proto_host_port[1]); + let host_port: Vec<&str> = host_port.split(":").collect(); + if host_port.len() != 2 { + return Err(CliError::ArgsErr("invalid value for --endpoint".into())); + } + let (host, port) = (host_port[0], host_port[1]); + let port = match port.parse::() { + Ok(port) => port, + Err(e) => { + return Err(CliError::ArgsErr(format!( + "invalid value for endpoint port. {e}" + ))) + } + }; + let tls_cert = args.remove("--tls-cert"); + match protocol { + "tcp" => { + // TODO(@ohsayan): warn! + ClientConfigKind::Tcp(host.into(), port) + } + "tls" => { + // we need a TLS cert + match tls_cert { + Some(path) => { + let cert = fs::read_to_string(path)?; + ClientConfigKind::Tls(host.into(), port, cert) + } + None => { + return Err(CliError::ArgsErr(format!( + "must provide TLS cert when using TLS endpoint" + ))) + } + } + } + _ => { + return Err(CliError::ArgsErr(format!( + "unknown protocol scheme `{protocol}`" + ))) + } + } + } + }; + let username = match args.remove("--user") { + Some(u) => u, + None => { + // default + "root".into() + } + }; + let password = match args.remove("--password") { + Some(p) => p, + None => read_password("Enter password: ")?, + }; + if args.is_empty() { + Ok(Task::OpenShell(ClientConfig::new( + endpoint, username, password, + ))) + } else { + Err(CliError::ArgsErr(format!("found unknown arguments"))) + } +} + +fn read_password(prompt: &str) -> Result { + terminal::enable_raw_mode()?; + print!("{prompt}"); + io::stdout().flush()?; + let mut password = String::new(); + loop { + match event::read()? { + Event::Key(KeyEvent { + code: KeyCode::Char('c'), + modifiers: event::KeyModifiers::CONTROL, + .. + }) => { + terminal::disable_raw_mode()?; + println!(); + exit(0x00) + } + Event::Key(KeyEvent { code, .. }) => match code { + KeyCode::Backspace => { + let _ = password.pop(); + } + KeyCode::Char(c) => password.push(c), + KeyCode::Enter => break, + _ => {} + }, + _ => {} + } + } + terminal::disable_raw_mode()?; + println!(); + Ok(password) +} diff --git a/cli/src/error.rs b/cli/src/error.rs new file mode 100644 index 00000000..89878bac --- /dev/null +++ b/cli/src/error.rs @@ -0,0 +1,60 @@ +/* + * Created on Wed Nov 15 2023 + * + * This file is a part of Skytable + * Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source + * NoSQL database written by Sayan Nandan ("the Author") with the + * vision to provide flexibility in data modelling without compromising + * on performance, queryability or scalability. + * + * Copyright (c) 2023, Sayan Nandan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * +*/ + +use core::fmt; + +pub type CliResult = Result; + +#[derive(Debug)] +pub enum CliError { + QueryError(String), + ArgsErr(String), + ClientError(skytable::error::Error), + IoError(std::io::Error), +} + +impl From for CliError { + fn from(cle: skytable::error::Error) -> Self { + Self::ClientError(cle) + } +} + +impl From for CliError { + fn from(ioe: std::io::Error) -> Self { + Self::IoError(ioe) + } +} + +impl fmt::Display for CliError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ArgsErr(e) => write!(f, "incorrect arguments. {e}"), + Self::ClientError(e) => write!(f, "client error. {e}"), + Self::IoError(e) => write!(f, "i/o error. {e}"), + Self::QueryError(e) => write!(f, "invalid query. {e}"), + } + } +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 57a21b8e..c23a09b6 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -24,4 +24,32 @@ * */ -fn main() {} +macro_rules! fatal { + ($($arg:tt)*) => {{ + eprintln!($($arg)*); + std::process::exit(0x01); + }} +} + +mod args; +mod error; +mod query; +mod repl; +mod resp; + +use args::Task; + +fn main() { + match run() { + Ok(()) => {} + Err(e) => fatal!("cli error: {e}"), + } +} + +fn run() -> error::CliResult<()> { + match args::parse()? { + Task::HelpMessage(msg) => println!("{msg}"), + Task::OpenShell(cfg) => repl::start(cfg)?, + } + Ok(()) +} diff --git a/cli/src/query.rs b/cli/src/query.rs new file mode 100644 index 00000000..db272493 --- /dev/null +++ b/cli/src/query.rs @@ -0,0 +1,266 @@ +/* + * Created on Thu Nov 16 2023 + * + * This file is a part of Skytable + * Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source + * NoSQL database written by Sayan Nandan ("the Author") with the + * vision to provide flexibility in data modelling without compromising + * on performance, queryability or scalability. + * + * Copyright (c) 2023, Sayan Nandan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * +*/ + +use { + crate::error::{CliError, CliResult}, + skytable::{ + error::ClientResult, query::SQParam, response::Response, Connection, ConnectionTls, Query, + }, +}; + +pub trait IsConnection { + fn execute_query(&mut self, q: Query) -> ClientResult; +} + +impl IsConnection for Connection { + fn execute_query(&mut self, q: Query) -> ClientResult { + self.query(&q) + } +} + +impl IsConnection for ConnectionTls { + fn execute_query(&mut self, q: Query) -> ClientResult { + self.query(&q) + } +} + +#[derive(Debug, PartialEq)] +enum Item { + UInt(u64), + SInt(i64), + Float(f64), + String(String), + Bin(Vec), +} + +impl SQParam for Item { + fn push(self, buf: &mut Vec) { + match self { + Item::UInt(u) => u.push(buf), + Item::SInt(s) => s.push(buf), + Item::Float(f) => f.push(buf), + Item::String(s) => s.push(buf), + Item::Bin(b) => SQParam::push(&*b, buf), + } + } +} + +pub struct Parameterizer { + buf: Vec, + i: usize, + params: Vec, + query: Vec, +} + +impl Parameterizer { + pub fn new(q: String) -> Self { + Self { + buf: q.into_bytes(), + i: 0, + params: vec![], + query: vec![], + } + } + pub fn parameterize(mut self) -> CliResult { + while self.not_eof() { + match self.buf[self.i] { + b if b.is_ascii_alphabetic() || b == b'_' => self.read_ident(), + b if b.is_ascii_digit() => self.read_unsigned_integer(), + b'-' => self.read_signed_integer(), + quote_style @ (b'"' | b'\'') => { + self.i += 1; + self.read_string(quote_style) + } + b'`' => { + self.i += 1; + self.read_binary() + } + sym => { + self.i += 1; + self.query.push(sym); + Ok(()) + } + }? + } + match String::from_utf8(self.query) { + Ok(qstr) => { + let mut q = Query::new(&qstr); + self.params.into_iter().for_each(|p| { + q.push_param(p); + }); + Ok(q) + } + Err(_) => Err(CliError::QueryError("query is not valid UTF-8".into())), + } + } + fn read_string(&mut self, quote_style: u8) -> CliResult<()> { + self.query.push(b'?'); + let mut string = Vec::new(); + let mut terminated = false; + while self.not_eof() && !terminated { + let b = self.buf[self.i]; + if b == b'\\' { + self.i += 1; + // escape sequence + if self.i == self.buf.len() { + // string was not terminated + return Err(CliError::QueryError("string not terminated".into())); + } + match self.buf[self.i] { + b'\\' => { + // escaped \ + string.push(b'\\'); + } + b if b == quote_style => { + // escape quote + string.push(quote_style); + } + _ => return Err(CliError::QueryError("unknown escape sequence".into())), + } + } + if b == quote_style { + terminated = true; + } else { + string.push(b); + } + self.i += 1; + } + if terminated { + match String::from_utf8(string) { + Ok(s) => self.params.push(Item::String(s)), + Err(_) => return Err(CliError::QueryError("invalid UTF-8 string".into())), + } + Ok(()) + } else { + return Err(CliError::QueryError("string not terminated".into())); + } + } + fn read_ident(&mut self) -> CliResult<()> { + // we're looking at an ident + let start = self.i; + self.i += 1; + while self.not_eof() { + if self.buf[self.i].is_ascii_alphanumeric() || self.buf[self.i] == b'_' { + self.i += 1; + } else { + break; + } + } + let stop = self.i; + self.query.extend(&self.buf[start..stop]); + Ok(()) + } + fn read_float(&mut self, start: usize) -> CliResult<()> { + self.read_until_number_escape(); + let stop = self.i; + match core::str::from_utf8(&self.buf[start..stop]).map(|v| v.parse()) { + Ok(Ok(num)) => self.params.push(Item::Float(num)), + _ => { + return Err(CliError::QueryError( + "invalid floating point literal".into(), + )) + } + } + Ok(()) + } + fn read_signed_integer(&mut self) -> CliResult<()> { + self.query.push(b'?'); + // we must have encountered a `-` + let start = self.i; + self.read_until_number_escape(); + let stop = self.i; + match core::str::from_utf8(&self.buf[start..stop]).map(|v| v.parse()) { + Ok(Ok(s)) => self.params.push(Item::SInt(s)), + _ => { + return Err(CliError::QueryError( + "invalid signed integer literal".into(), + )) + } + } + Ok(()) + } + fn read_unsigned_integer(&mut self) -> CliResult<()> { + self.query.push(b'?'); + let start = self.i; + let mut ret = 0u64; + while self.not_eof() { + match self.buf[self.i] { + b if b.is_ascii_digit() => { + self.i += 1; + ret = match ret + .checked_mul(10) + .map(|v| v.checked_add((b & 0x0f) as u64)) + { + Some(Some(r)) => r, + _ => return Err(CliError::QueryError("bad value for integer".into())), + }; + } + b'.' => { + self.i += 1; + // uh oh, that's a float + return self.read_float(start); + } + b if b == b' ' || b == b'\t' || b.is_ascii_punctuation() => { + break; + } + _ => { + // nothing else is valid here + return Err(CliError::QueryError( + "invalid unsigned integer literal".into(), + )); + } + } + } + self.params.push(Item::UInt(ret)); + Ok(()) + } + fn read_until_number_escape(&mut self) { + while self.not_eof() { + let b = self.buf[self.i]; + if b == b'\n' || b == b'\t' || b.is_ascii_punctuation() { + break; + } + self.i += 1; + } + } + fn read_binary(&mut self) -> CliResult<()> { + self.query.push(b'?'); + let start = self.i; + while self.not_eof() { + let b = self.buf[self.i]; + self.i += 1; + if b == b'`' { + self.params + .push(Item::Bin(self.buf[start..self.i].to_vec())); + return Ok(()); + } + } + Err(CliError::QueryError("binary literal not terminated".into())) + } + fn not_eof(&self) -> bool { + self.i < self.buf.len() + } +} diff --git a/cli/src/repl.rs b/cli/src/repl.rs new file mode 100644 index 00000000..48ea52b6 --- /dev/null +++ b/cli/src/repl.rs @@ -0,0 +1,137 @@ +/* + * Created on Thu Nov 16 2023 + * + * This file is a part of Skytable + * Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source + * NoSQL database written by Sayan Nandan ("the Author") with the + * vision to provide flexibility in data modelling without compromising + * on performance, queryability or scalability. + * + * Copyright (c) 2023, Sayan Nandan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * +*/ + +use { + crate::{ + args::{ClientConfig, ClientConfigKind}, + error::{CliError, CliResult}, + query::{self, IsConnection}, + resp, + }, + crossterm::{cursor, execute, terminal}, + rustyline::{config::Configurer, error::ReadlineError, DefaultEditor}, + skytable::Config, + std::io::{stdout, ErrorKind}, +}; + +const SKYSH_HISTORY_FILE: &str = ".sky_history"; +const TXT_WELCOME: &str = include_str!("../help_text/welcome"); + +pub fn start(cfg: ClientConfig) -> CliResult<()> { + match cfg.kind { + ClientConfigKind::Tcp(host, port) => { + let c = Config::new(&host, port, &cfg.username, &cfg.password).connect()?; + println!( + "Authenticated as '{}' on {}:{} over Skyhash/TCP\n---", + &cfg.username, &host, &port + ); + repl(c) + } + ClientConfigKind::Tls(host, port, cert) => { + let c = Config::new(&host, port, &cfg.username, &cfg.password).connect_tls(&cert)?; + println!( + "Authenticated as '{}' on {}:{} over Skyhash/TLS\n---", + &cfg.username, &host, &port + ); + repl(c) + } + } +} + +fn repl(mut con: C) -> CliResult<()> { + let init_editor = || { + let mut editor = DefaultEditor::new()?; + editor.set_auto_add_history(true); + editor.set_history_ignore_dups(true)?; + editor.bind_sequence( + rustyline::KeyEvent( + rustyline::KeyCode::BracketedPasteStart, + rustyline::Modifiers::NONE, + ), + rustyline::Cmd::Noop, + ); + match editor.load_history(SKYSH_HISTORY_FILE) { + Ok(()) => {} + Err(e) => match e { + ReadlineError::Io(ref ioe) => match ioe.kind() { + ErrorKind::NotFound => { + println!("{TXT_WELCOME}"); + } + _ => return Err(e), + }, + e => return Err(e), + }, + } + rustyline::Result::Ok(editor) + }; + let mut editor = match init_editor() { + Ok(e) => e, + Err(e) => fatal!("error: failed to init REPL. {e}"), + }; + loop { + match editor.readline("> ") { + Ok(line) => match line.as_str() { + "!help" => println!("{TXT_WELCOME}"), + "exit" => break, + "clear" => clear_screen()?, + _ => { + if line.is_empty() { + continue; + } + match query::Parameterizer::new(line).parameterize() { + Ok(q) => resp::format_response(con.execute_query(q)?)?, + Err(e) => match e { + CliError::QueryError(e) => { + eprintln!("[skysh error]: bad query. {e}"); + continue; + } + _ => return Err(e), + }, + }; + } + }, + Err(e) => match e { + ReadlineError::Interrupted | ReadlineError::Eof => { + // done + break; + } + ReadlineError::WindowResized => {} + e => fatal!("error: failed to read line REPL. {e}"), + }, + } + } + editor + .save_history(SKYSH_HISTORY_FILE) + .expect("failed to save history"); + println!("Goodbye!"); + Ok(()) +} + +fn clear_screen() -> std::io::Result<()> { + let mut stdout = stdout(); + execute!(stdout, terminal::Clear(terminal::ClearType::All))?; + execute!(stdout, cursor::MoveTo(0, 0)) +} diff --git a/cli/src/resp.rs b/cli/src/resp.rs new file mode 100644 index 00000000..c35f3374 --- /dev/null +++ b/cli/src/resp.rs @@ -0,0 +1,144 @@ +/* + * Created on Thu Nov 16 2023 + * + * This file is a part of Skytable + * Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source + * NoSQL database written by Sayan Nandan ("the Author") with the + * vision to provide flexibility in data modelling without compromising + * on performance, queryability or scalability. + * + * Copyright (c) 2023, Sayan Nandan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * +*/ + +use { + crate::error::CliResult, + crossterm::{ + style::{Color, ResetColor, SetForegroundColor}, + ExecutableCommand, + }, + skytable::response::{Response, Row, Value}, + std::io::{self, Write}, +}; + +pub fn format_response(resp: Response) -> CliResult<()> { + match resp { + Response::Empty => print_cyan("(Okay)\n")?, + Response::Error(e) => print_red(&format!("(server error code: {e})\n"))?, + Response::Value(v) => { + print_value(v)?; + println!(); + } + Response::Row(r) => { + print_row(r)?; + println!(); + } + }; + Ok(()) +} + +fn print_row(r: Row) -> CliResult<()> { + print!("("); + let mut columns = r.into_values().into_iter().peekable(); + while let Some(cell) = columns.next() { + print_value(cell)?; + if columns.peek().is_some() { + print!(", "); + } + } + print!(")"); + Ok(()) +} + +fn print_value(v: Value) -> CliResult<()> { + match v { + Value::Null => print_gray("null")?, + Value::String(s) => print_string(&s), + Value::Binary(b) => print_binary(&b), + Value::Bool(b) => print!("{b}"), + Value::UInt8(i) => print!("{i}"), + Value::UInt16(i) => print!("{i}"), + Value::UInt32(i) => print!("{i}"), + Value::UInt64(i) => print!("{i}"), + Value::SInt8(i) => print!("{i}"), + Value::SInt16(i) => print!("{i}"), + Value::SInt32(i) => print!("{i}"), + Value::SInt64(i) => print!("{i}"), + Value::Float32(f) => print!("{f}"), + Value::Float64(f) => print!("{f}"), + Value::List(items) => { + print!("["); + let mut items = items.into_iter().peekable(); + while let Some(item) = items.next() { + print_value(item)?; + if items.peek().is_some() { + print!(", "); + } + } + print!("]"); + } + } + Ok(()) +} + +fn print_binary(b: &[u8]) { + let mut it = b.into_iter().peekable(); + print!("["); + while let Some(byte) = it.next() { + print!("{byte}"); + if it.peek().is_some() { + print!(", "); + } + } + print!("]"); +} + +fn print_string(s: &str) { + print!("\""); + for ch in s.chars() { + if ch == '"' || ch == '\'' { + print!("\\{ch}"); + } else if ch == '\t' { + print!("\\t"); + } else if ch == '\n' { + print!("\\n"); + } else { + print!("{ch}"); + } + } + print!("\""); +} + +fn print_gray(s: &str) -> std::io::Result<()> { + print_colored_text(s, Color::White) +} + +fn print_red(s: &str) -> std::io::Result<()> { + print_colored_text(s, Color::Red) +} + +fn print_cyan(s: &str) -> std::io::Result<()> { + print_colored_text(s, Color::Cyan) +} + +fn print_colored_text(text: &str, color: Color) -> std::io::Result<()> { + let mut stdout = io::stdout(); + stdout.execute(SetForegroundColor(color))?; + print!("{text}"); + stdout.flush()?; + stdout.execute(ResetColor)?; + Ok(()) +} diff --git a/server/src/engine/mem/scanner.rs b/server/src/engine/mem/scanner.rs index e6fb1bfb..48332b64 100644 --- a/server/src/engine/mem/scanner.rs +++ b/server/src/engine/mem/scanner.rs @@ -104,6 +104,9 @@ impl<'a, T> Scanner<'a, T> { } impl<'a, T> Scanner<'a, T> { + pub fn inner_buffer(&self) -> &'a [T] { + &self.d + } /// Manually set the cursor position /// /// ## Safety diff --git a/server/src/engine/ql/lex/mod.rs b/server/src/engine/ql/lex/mod.rs index f25a3ad3..3ca0b428 100644 --- a/server/src/engine/ql/lex/mod.rs +++ b/server/src/engine/ql/lex/mod.rs @@ -442,32 +442,26 @@ static SCAN_PARAM: [unsafe fn(&mut SecureLexer); 8] = unsafe { if okay { slf.l.push_token(Lit::new_sint(int)) } else { - slf.l.set_error(QueryError::LexInvalidLiteral) + slf.l.set_error(QueryError::LexInvalidParameter) } }, // float |slf| { - let Some(size_of_body) = slf - .param_buffer - .try_next_ascii_u64_lf_separated_or_restore_cursor() - else { - slf.l.set_error(QueryError::LexInvalidParameter); - return; - }; - let body = match slf - .param_buffer - .try_next_variable_block(size_of_body as usize) - { - Some(body) => body, - None => { - slf.l.set_error(QueryError::LexInvalidParameter); + let start = slf.param_buffer.cursor(); + while !slf.param_buffer.eof() { + let cursor = slf.param_buffer.cursor(); + let byte = slf.param_buffer.next_byte(); + if byte == b'\n' { + match core::str::from_utf8(&slf.param_buffer.inner_buffer()[start..cursor]) + .map(core::str::FromStr::from_str) + { + Ok(Ok(f)) => slf.l.push_token(Lit::new_float(f)), + _ => slf.l.set_error(QueryError::LexInvalidParameter), + } return; } - }; - match core::str::from_utf8(body).map(core::str::FromStr::from_str) { - Ok(Ok(fp)) => slf.l.push_token(Lit::new_float(fp)), - _ => slf.l.set_error(QueryError::LexInvalidParameter), } + slf.l.set_error(QueryError::LexInvalidParameter) }, // binary |slf| { diff --git a/server/src/engine/ql/tests/lexer_tests.rs b/server/src/engine/ql/tests/lexer_tests.rs index d5e3e121..0985e101 100644 --- a/server/src/engine/ql/tests/lexer_tests.rs +++ b/server/src/engine/ql/tests/lexer_tests.rs @@ -195,7 +195,7 @@ fn make_safe_query(a: &[u8], b: &[u8]) -> (Vec, usize) { fn safe_query_all_literals() { let (query, query_window) = make_safe_query( b"? ? ? ? ? ? ?", - b"\x00\x01\x01\x021234\n\x03-1234\n\x049\n1234.5678\x0513\nbinarywithlf\n\x065\nsayan", + b"\x00\x01\x01\x021234\n\x03-1234\n\x041234.5678\n\x0513\nbinarywithlf\n\x065\nsayan", ); let ret = lex_secure(&query, query_window).unwrap(); assert_eq!(