Implement new REPL and remove old impls

next
Sayan Nandan 10 months ago
parent fff5780586
commit fddb24ad0b
No known key found for this signature in database
GPG Key ID: 42EEDF4AE9D96B54

2
.gitignore vendored

@ -16,3 +16,5 @@ snapstore.partmap
.skytest_* .skytest_*
*.pem *.pem
passphrase.txt passphrase.txt
*.db-tlog
*.db

175
Cargo.lock generated

@ -184,6 +184,17 @@ dependencies = [
"inout", "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]] [[package]]
name = "constant_time_eq" name = "constant_time_eq"
version = "0.1.5" version = "0.1.5"
@ -282,6 +293,31 @@ dependencies = [
"cfg-if", "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]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@ -315,6 +351,12 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "endian-type"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.10.0" version = "0.10.0"
@ -355,12 +397,33 @@ dependencies = [
"libc", "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]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.0.1" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" 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]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.26" version = "1.0.26"
@ -482,6 +545,15 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "home"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
dependencies = [
"windows-sys",
]
[[package]] [[package]]
name = "humantime" name = "humantime"
version = "2.1.0" version = "2.1.0"
@ -632,6 +704,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
dependencies = [ dependencies = [
"libc", "libc",
"log",
"wasi", "wasi",
"windows-sys", "windows-sys",
] ]
@ -654,6 +727,26 @@ dependencies = [
"tempfile", "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]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.16.0" version = "1.16.0"
@ -838,6 +931,16 @@ dependencies = [
"scheduled-thread-pool", "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]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -958,6 +1061,29 @@ dependencies = [
"windows-sys", "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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.15" version = "1.0.15"
@ -1066,6 +1192,27 @@ dependencies = [
"digest", "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]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.1" version = "1.4.1"
@ -1123,14 +1270,16 @@ dependencies = [
name = "skysh" name = "skysh"
version = "0.8.0" version = "0.8.0"
dependencies = [ dependencies = [
"crossterm",
"libsky", "libsky",
"rustyline",
"skytable", "skytable",
] ]
[[package]] [[package]]
name = "skytable" name = "skytable"
version = "0.8.0" 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 = [ dependencies = [
"async-trait", "async-trait",
"bb8", "bb8",
@ -1166,6 +1315,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "str-buf"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.5.0" version = "2.5.0"
@ -1297,12 +1452,30 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 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]] [[package]]
name = "unsafe-libyaml" name = "unsafe-libyaml"
version = "0.2.9" version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.4.1" version = "1.4.1"

@ -10,3 +10,5 @@ edition = "2021"
# internal deps # internal deps
libsky = { path = "../libsky" } libsky = { path = "../libsky" }
skytable = { git = "https://github.com/skytable/client-rust.git", branch = "octave" } skytable = { git = "https://github.com/skytable/client-rust.git", branch = "octave" }
crossterm = "0.27.0"
rustyline = "12.0.0"

@ -0,0 +1,30 @@
skysh 0.8.0
Sayan N. <ohsayan@outlook.com>
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

@ -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!

@ -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 <ohsayan@outlook.com>
*
* 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 <https://www.gnu.org/licenses/>.
*
*/
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<String, String>),
}
fn load_env() -> CliResult<TaskInner> {
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<Task> {
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::<u16>() {
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<String, std::io::Error> {
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)
}

@ -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 <ohsayan@outlook.com>
*
* 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 <https://www.gnu.org/licenses/>.
*
*/
use core::fmt;
pub type CliResult<T> = Result<T, CliError>;
#[derive(Debug)]
pub enum CliError {
QueryError(String),
ArgsErr(String),
ClientError(skytable::error::Error),
IoError(std::io::Error),
}
impl From<skytable::error::Error> for CliError {
fn from(cle: skytable::error::Error) -> Self {
Self::ClientError(cle)
}
}
impl From<std::io::Error> 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}"),
}
}
}

@ -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(())
}

@ -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 <ohsayan@outlook.com>
*
* 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 <https://www.gnu.org/licenses/>.
*
*/
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<Response>;
}
impl IsConnection for Connection {
fn execute_query(&mut self, q: Query) -> ClientResult<Response> {
self.query(&q)
}
}
impl IsConnection for ConnectionTls {
fn execute_query(&mut self, q: Query) -> ClientResult<Response> {
self.query(&q)
}
}
#[derive(Debug, PartialEq)]
enum Item {
UInt(u64),
SInt(i64),
Float(f64),
String(String),
Bin(Vec<u8>),
}
impl SQParam for Item {
fn push(self, buf: &mut Vec<u8>) {
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<u8>,
i: usize,
params: Vec<Item>,
query: Vec<u8>,
}
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<Query> {
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()
}
}

@ -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 <ohsayan@outlook.com>
*
* 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 <https://www.gnu.org/licenses/>.
*
*/
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<C: IsConnection>(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))
}

@ -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 <ohsayan@outlook.com>
*
* 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 <https://www.gnu.org/licenses/>.
*
*/
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(())
}

@ -104,6 +104,9 @@ impl<'a, T> Scanner<'a, T> {
} }
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 /// Manually set the cursor position
/// ///
/// ## Safety /// ## Safety

@ -442,32 +442,26 @@ static SCAN_PARAM: [unsafe fn(&mut SecureLexer); 8] = unsafe {
if okay { if okay {
slf.l.push_token(Lit::new_sint(int)) slf.l.push_token(Lit::new_sint(int))
} else { } else {
slf.l.set_error(QueryError::LexInvalidLiteral) slf.l.set_error(QueryError::LexInvalidParameter)
} }
}, },
// float // float
|slf| { |slf| {
let Some(size_of_body) = slf let start = slf.param_buffer.cursor();
.param_buffer while !slf.param_buffer.eof() {
.try_next_ascii_u64_lf_separated_or_restore_cursor() let cursor = slf.param_buffer.cursor();
else { let byte = slf.param_buffer.next_byte();
slf.l.set_error(QueryError::LexInvalidParameter); if byte == b'\n' {
return; match core::str::from_utf8(&slf.param_buffer.inner_buffer()[start..cursor])
}; .map(core::str::FromStr::from_str)
let body = match slf
.param_buffer
.try_next_variable_block(size_of_body as usize)
{ {
Some(body) => body, Ok(Ok(f)) => slf.l.push_token(Lit::new_float(f)),
None => { _ => slf.l.set_error(QueryError::LexInvalidParameter),
slf.l.set_error(QueryError::LexInvalidParameter); }
return; 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 // binary
|slf| { |slf| {

@ -195,7 +195,7 @@ fn make_safe_query(a: &[u8], b: &[u8]) -> (Vec<u8>, usize) {
fn safe_query_all_literals() { fn safe_query_all_literals() {
let (query, query_window) = make_safe_query( let (query, query_window) = make_safe_query(
b"? ? ? ? ? ? ?", 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(); let ret = lex_secure(&query, query_window).unwrap();
assert_eq!( assert_eq!(

Loading…
Cancel
Save