mirror of
https://github.com/fluencelabs/llamadb-wasm
synced 2025-04-25 06:42:21 +00:00
initial commit
This commit is contained in:
commit
86a35b062c
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
target
|
||||
Cargo.lock
|
||||
*.db
|
||||
|
||||
.idea/
|
||||
*.iml
|
||||
*.ipr
|
||||
|
27
Cargo.toml
Normal file
27
Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "llama_db"
|
||||
version = "0.1.0"
|
||||
authors = ["Fluence Labs"]
|
||||
publish = false
|
||||
description = "LlamaDb wrapper for running into Fluence WasmVm"
|
||||
edition = "2018"
|
||||
|
||||
[profile.release]
|
||||
debug = false
|
||||
lto = true
|
||||
opt-level = 3
|
||||
debug-assertions = false
|
||||
overflow-checks = false
|
||||
panic = "abort"
|
||||
|
||||
[lib]
|
||||
name = "llama_db"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.1.0"
|
||||
fluence = { version = "0.1.0" }
|
||||
llamadb = { git = "https://github.com/fluencelabs/llamadb.git", branch = "master" }
|
||||
libsecp256k1 = "0.2.2"
|
||||
sha2 = "0.8.0"
|
93
src/lib.rs
Normal file
93
src/lib.rs
Normal file
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2018 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//! Wrapper for Llamadb (a test for Fluence network).
|
||||
//!
|
||||
//! Provides the FFI (`main`) for interact with Llamadb.
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
use std::error::Error;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use fluence::sdk::*;
|
||||
use llamadb::tempdb::{ExecuteStatementResponse, TempDb};
|
||||
|
||||
use crate::signature::*;
|
||||
|
||||
mod signature;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Result for all possible Error types.
|
||||
type GenResult<T> = ::std::result::Result<T, Box<Error>>;
|
||||
|
||||
lazy_static! {
|
||||
static ref DATABASE: Mutex<TempDb> = Mutex::new(TempDb::new());
|
||||
}
|
||||
|
||||
/// Flag to toggle signature validation
|
||||
static CHECK_SIGNATURE: bool = false;
|
||||
|
||||
/// Executes SQL and converts llamadb error to string.
|
||||
#[invocation_handler]
|
||||
fn main(input: String) -> String {
|
||||
let result = if CHECK_SIGNATURE {
|
||||
check_input(&input).and_then(run_query)
|
||||
} else {
|
||||
run_query(&input)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(response) => response,
|
||||
Err(err_msg) => format!("[Error] {}", err_msg),
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquires lock, does query, releases lock, returns query result.
|
||||
fn run_query(sql_query: &str) -> GenResult<String> {
|
||||
let mut db = DATABASE.lock()?;
|
||||
db.do_query(sql_query)
|
||||
.map(statement_to_string)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Converts query result to CSV String.
|
||||
fn statement_to_string(statement: ExecuteStatementResponse) -> String {
|
||||
match statement {
|
||||
ExecuteStatementResponse::Created => "table created".to_string(),
|
||||
ExecuteStatementResponse::Dropped => "table was dropped".to_string(),
|
||||
ExecuteStatementResponse::Inserted(number) => format!("rows inserted: {}", number),
|
||||
ExecuteStatementResponse::Select { column_names, rows } => {
|
||||
let col_names = column_names.to_vec().join(", ") + "\n";
|
||||
let rows_as_str = rows
|
||||
.map(|row| {
|
||||
row.iter()
|
||||
.map(|elem| elem.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
col_names + &rows_as_str
|
||||
}
|
||||
ExecuteStatementResponse::Deleted(number) => format!("rows deleted: {}", number),
|
||||
ExecuteStatementResponse::Explain(result) => result,
|
||||
ExecuteStatementResponse::Updated(number) => format!("rows updated: {}", number),
|
||||
}
|
||||
}
|
119
src/signature.rs
Normal file
119
src/signature.rs
Normal file
@ -0,0 +1,119 @@
|
||||
use crate::GenResult;
|
||||
use core::fmt;
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use secp256k1::{verify, Message, PublicKey, Signature};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Error(String);
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Representation of a signed request.
|
||||
/// Format is: signature\nnonce\npayload
|
||||
/// nonce and payload are concatenated here to avoid reallocation on hashing
|
||||
struct Signed<'a> {
|
||||
signature: &'a str,
|
||||
nonce_payload: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Signed<'a> {
|
||||
pub fn payload(&self) -> GenResult<&'a str> {
|
||||
let pos = self.nonce_payload.find("\n").ok_or(err_msg(&format!(
|
||||
"Invalid input: no \\n between nonce and payload in `{}`. \
|
||||
Should be <signature hex>\\n<nonce>\\n<sql_query>",
|
||||
self.nonce_payload
|
||||
)))?;
|
||||
Ok(&self.nonce_payload[pos + 1..])
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref PK: PublicKey = get_pk();
|
||||
}
|
||||
|
||||
/// hard-coded public key, could be replaced directly in a final Wasm binary
|
||||
// Full: 64 + 1 byte prefix
|
||||
static PUBLIC_KEY: [u8; 65] = [
|
||||
0x04,
|
||||
0xba,0x94,0x28,0x52,0xe4,0x35,0x39,0x1b,0x2a,0x6a,0x99,0x3f,0x33,0xf7,0x0c,0x43,
|
||||
0x92,0x35,0xf4,0x84,0x15,0xe2,0x5c,0x66,0x8d,0x97,0xd5,0xb0,0xf2,0x63,0xb5,0x4e,
|
||||
0x9d,0x98,0xcc,0xae,0xfd,0xc5,0xc6,0xca,0x23,0x7f,0xfc,0x0f,0x2f,0x63,0x35,0x13,
|
||||
0x30,0xfa,0xaf,0xe3,0x1d,0x12,0x03,0x79,0xb8,0xdf,0xf9,0x82,0xa3,0x73,0x58,0xe3
|
||||
];
|
||||
|
||||
fn get_pk() -> PublicKey {
|
||||
PublicKey::parse_slice(&PUBLIC_KEY, None).expect("Invalid public key")
|
||||
}
|
||||
|
||||
fn err_msg(s: &str) -> Box<Error> {
|
||||
Error(s.to_string()).into()
|
||||
}
|
||||
|
||||
/// Converts hex string to a Vec<u8>
|
||||
fn decode_hex(s: &str) -> GenResult<Vec<u8>> {
|
||||
if s.len() % 2 != 0 {
|
||||
return Err(err_msg(&format!("Invalid hex length: {} isn't divisible by 2", s.len())))
|
||||
}
|
||||
(0..s.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&s[i..i + 2], 16))
|
||||
.collect::<Result<Vec<u8>, ParseIntError>>()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// SHA-256 hash of the string
|
||||
fn hash_message(message: &str) -> [u8; 32] {
|
||||
let mut sha = Sha256::default();
|
||||
sha.input(message.as_bytes());
|
||||
let hash = sha.result();
|
||||
let mut result = [0; 32];
|
||||
//TODO: is there a better way for GenericArray<u8, 32> -> [u8; 32] ?
|
||||
result.copy_from_slice(hash.as_slice());
|
||||
result
|
||||
}
|
||||
|
||||
/// Verifies if signature is correct
|
||||
fn check_signature(hash: &[u8; 32], signature: &str) -> GenResult<bool> {
|
||||
let signature = decode_hex(signature)?;
|
||||
let signature = Signature::parse_slice(signature.as_slice())
|
||||
.map_err(|e| err_msg(&format!("Error parsing signature: {:?}", e)))?;
|
||||
let message = Message::parse(hash);
|
||||
|
||||
Ok(verify(&message, &signature, &PK))
|
||||
}
|
||||
|
||||
/// Parse input as `signature\nnonce\npayload`
|
||||
fn parse_signed(input: &String) -> GenResult<Signed> {
|
||||
let pos: usize = input.find("\n").ok_or(err_msg(&format!(
|
||||
"Invalid input: no '\\n' between signature and nonce in `{}`. \
|
||||
Should be <signature hex>\\n<nonce>\\n<sql_query>",
|
||||
input
|
||||
)))?;
|
||||
let signature: &str = &input[..pos];
|
||||
let nonce_payload: &str = &input[pos + 1..];
|
||||
Ok(Signed {
|
||||
signature,
|
||||
nonce_payload,
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks if input is signed
|
||||
/// returns payload string on success
|
||||
/// throws an error on failure
|
||||
pub fn check_input(input: &String) -> GenResult<&str> {
|
||||
let signed = parse_signed(input)?;
|
||||
let hash = hash_message(signed.nonce_payload);
|
||||
if check_signature(&hash, signed.signature)? {
|
||||
signed.payload()
|
||||
} else {
|
||||
Err(err_msg("Invalid signature"))
|
||||
}
|
||||
}
|
160
src/tests.rs
Normal file
160
src/tests.rs
Normal file
@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2018 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//! Module with tests.
|
||||
|
||||
#[test]
|
||||
fn integration_sql_test() {
|
||||
//
|
||||
// Success cases.
|
||||
//
|
||||
|
||||
let create_table = execute_sql("CREATE TABLE Users(id INT, name TEXT, age INT)");
|
||||
assert_eq!(create_table, "table created");
|
||||
|
||||
let insert_one = execute_sql("INSERT INTO Users VALUES(1, 'Sara', 23)");
|
||||
assert_eq!(insert_one, "rows inserted: 1");
|
||||
|
||||
let insert_several =
|
||||
execute_sql("INSERT INTO Users VALUES(2, 'Bob', 19), (3, 'Caroline', 31), (4, 'Max', 25)");
|
||||
assert_eq!(insert_several, "rows inserted: 3");
|
||||
|
||||
let create_table_role = execute_sql("CREATE TABLE Roles(user_id INT, role VARCHAR(128))");
|
||||
assert_eq!(create_table_role, "table created");
|
||||
|
||||
let insert_roles = execute_sql(
|
||||
"INSERT INTO Roles VALUES(1, 'Teacher'), (2, 'Student'), (3, 'Scientist'), (4, 'Writer')",
|
||||
);
|
||||
assert_eq!(insert_roles, "rows inserted: 4");
|
||||
|
||||
let empty_select = execute_sql("SELECT * FROM Users WHERE name = 'unknown'");
|
||||
assert_eq!(empty_select, "id, name, age\n");
|
||||
|
||||
let select_all = execute_sql(
|
||||
"SELECT min(user_id) as min, max(user_id) as max, \
|
||||
count(user_id) as count, sum(user_id) as sum, avg(user_id) as avg FROM Roles",
|
||||
);
|
||||
assert_eq!(select_all, "min, max, count, sum, avg\n1, 4, 4, 10, 2.5");
|
||||
|
||||
let select_with_join = execute_sql(
|
||||
"SELECT u.name AS Name, r.role AS Role FROM Users u JOIN Roles \
|
||||
r ON u.id = r.user_id WHERE r.role = 'Writer'",
|
||||
);
|
||||
assert_eq!(select_with_join, "name, role\nMax, Writer");
|
||||
|
||||
let explain = execute_sql("EXPLAIN SELECT id, name FROM Users");
|
||||
assert_eq!(
|
||||
explain,
|
||||
"query plan\n".to_string()
|
||||
+ "column names: (`id`, `name`)\n"
|
||||
+ "(scan `users` :source-id 0\n"
|
||||
+ " (yield\n"
|
||||
+ " (column-field :source-id 0 :column-offset 0)\n"
|
||||
+ " (column-field :source-id 0 :column-offset 1)))"
|
||||
);
|
||||
|
||||
let delete = execute_sql(
|
||||
"DELETE FROM Users WHERE id = (SELECT user_id FROM Roles WHERE role = 'Student');",
|
||||
);
|
||||
assert_eq!(delete, "rows deleted: 1");
|
||||
|
||||
let update_query = execute_sql("UPDATE Users SET name = 'Min' WHERE name = 'Max'");
|
||||
assert_eq!(update_query, "rows updated: 1");
|
||||
|
||||
//
|
||||
// Error cases.
|
||||
//
|
||||
|
||||
let empty_str = execute_sql("");
|
||||
assert_eq!(empty_str, "[Error] Expected SELECT, INSERT, CREATE, DELETE, TRUNCATE or EXPLAIN statement; got no more tokens");
|
||||
|
||||
let invalid_sql = execute_sql("123");
|
||||
assert_eq!(invalid_sql, "[Error] Expected SELECT, INSERT, CREATE, DELETE, TRUNCATE or EXPLAIN statement; got Number(\"123\")");
|
||||
|
||||
let bad_query = execute_sql("SELECT salary FROM Users");
|
||||
assert_eq!(bad_query, "[Error] column does not exist: salary");
|
||||
|
||||
let lexer_error = execute_sql("π");
|
||||
assert_eq!(lexer_error, "[Error] Lexer error: Unknown character π");
|
||||
|
||||
let incompatible_types = execute_sql("SELECT * FROM Users WHERE age = 'Bob'");
|
||||
assert_eq!(
|
||||
incompatible_types,
|
||||
"[Error] 'Bob' cannot be cast to Integer { signed: true, bytes: 8 }"
|
||||
);
|
||||
|
||||
let not_supported_order_by = execute_sql("SELECT * FROM Users ORDER BY name");
|
||||
assert_eq!(
|
||||
not_supported_order_by,
|
||||
"[Error] order by in not implemented"
|
||||
);
|
||||
|
||||
let truncate = execute_sql("TRUNCATE TABLE Users");
|
||||
assert_eq!(truncate, "rows deleted: 3");
|
||||
|
||||
let drop_table = execute_sql("DROP TABLE Users");
|
||||
assert_eq!(drop_table, "table was dropped");
|
||||
|
||||
let select_by_dropped_table = execute_sql("SELECT * FROM Users");
|
||||
assert_eq!(
|
||||
select_by_dropped_table,
|
||||
"[Error] table does not exist: users"
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Private helper functions.
|
||||
//
|
||||
use std::mem;
|
||||
use std::ptr;
|
||||
|
||||
pub unsafe fn read_result_from_mem(ptr: *mut u8) -> Vec<u8> {
|
||||
const RESULT_SIZE_BYTES: usize = 4;
|
||||
|
||||
// read string length from current pointer
|
||||
let mut len_as_bytes: [u8; RESULT_SIZE_BYTES] = [0; RESULT_SIZE_BYTES];
|
||||
ptr::copy_nonoverlapping(ptr, len_as_bytes.as_mut_ptr(), RESULT_SIZE_BYTES);
|
||||
|
||||
let input_len: u32 = mem::transmute(len_as_bytes);
|
||||
let total_len = input_len as usize + RESULT_SIZE_BYTES;
|
||||
|
||||
// creates object from raw bytes
|
||||
let mut input = Vec::from_raw_parts(ptr, total_len, total_len);
|
||||
|
||||
// drains RESULT_SIZE_BYTES from the beginning of created vector, it allows to effectively
|
||||
// skips (without additional allocations) length of the result.
|
||||
{
|
||||
input.drain(0..4);
|
||||
}
|
||||
input
|
||||
}
|
||||
|
||||
/// Executes sql and returns result as a String.
|
||||
fn execute_sql(sql: &str) -> String {
|
||||
unsafe {
|
||||
use std::mem;
|
||||
|
||||
let mut sql_str = sql.to_string();
|
||||
// executes query
|
||||
let result_str_ptr = super::invoke(sql_str.as_bytes_mut().as_mut_ptr(), sql_str.len());
|
||||
// ownership of memory of 'sql_str' will be taken through 'ptr' inside
|
||||
// 'invoke' method, have to forget 'sql_str' for prevent deallocation
|
||||
mem::forget(sql_str);
|
||||
// converts results
|
||||
let result_str = read_result_from_mem(result_str_ptr.as_ptr());
|
||||
String::from_utf8(result_str).unwrap()
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user