From 99b90017b61fae9352668d9d70414fd0221bcfd5 Mon Sep 17 00:00:00 2001 From: Ben Ruijl Date: Sat, 1 Jun 2024 17:08:09 +0200 Subject: [PATCH] Add inline Var and Num construction - Support reading new license key format - Deprecate request_offline_key - Add get_license_key --- src/api/cpp.rs | 14 ++-- src/api/python.rs | 10 ++- src/atom.rs | 14 ++++ src/atom/representation.rs | 92 ++++++++++++++++++++++ src/lib.rs | 153 +++++++++++++++++++++++-------------- src/state.rs | 6 +- src/streaming.rs | 11 ++- 7 files changed, 230 insertions(+), 70 deletions(-) diff --git a/src/api/cpp.rs b/src/api/cpp.rs index 99863630..e9a7de76 100644 --- a/src/api/cpp.rs +++ b/src/api/cpp.rs @@ -1,4 +1,4 @@ -use std::ffi::{c_char, CStr, CString}; +use std::ffi::{c_char, CStr}; use std::fmt::Write; use std::os::raw::c_ulonglong; use std::sync::Arc; @@ -79,13 +79,11 @@ unsafe extern "C" fn request_trial_license( /// Get a license key for offline use, generated from a licensed Symbolica session. The key will remain valid for 24 hours. /// The key is written into `key`, which must be a buffer of at least 100 bytes. #[no_mangle] -unsafe extern "C" fn get_offline_license_key(key: *mut c_char) -> bool { - match LicenseManager::get_offline_license_key() { - Ok(k) => { - let cs = CString::new(k).unwrap(); - key.copy_from_nonoverlapping(cs.as_ptr(), cs.as_bytes_with_nul().len()); - true - } +unsafe extern "C" fn get_license_key(email: *const c_char) -> bool { + let email = unsafe { CStr::from_ptr(email) }.to_str().unwrap(); + + match LicenseManager::get_license_key(email) { + Ok(()) => true, Err(e) => { eprintln!("{}", e); false diff --git a/src/api/python.rs b/src/api/python.rs index fd159d89..a351e4fc 100644 --- a/src/api/python.rs +++ b/src/api/python.rs @@ -90,7 +90,7 @@ fn symbolica(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(request_hobbyist_license, m)?)?; m.add_function(wrap_pyfunction!(request_trial_license, m)?)?; m.add_function(wrap_pyfunction!(request_sublicense, m)?)?; - m.add_function(wrap_pyfunction!(get_offline_license_key, m)?)?; + m.add_function(wrap_pyfunction!(get_license_key, m)?)?; m.add("__version__", env!("CARGO_PKG_VERSION"))?; @@ -147,10 +147,12 @@ fn request_sublicense( .map_err(exceptions::PyConnectionError::new_err) } -/// Get a license key for offline use, generated from a licensed Symbolica session. The key will remain valid for 24 hours. +/// Get the license key for the account registered with the provided email address. #[pyfunction] -fn get_offline_license_key() -> PyResult { - LicenseManager::get_offline_license_key().map_err(exceptions::PyValueError::new_err) +fn get_license_key(email: String) -> PyResult<()> { + LicenseManager::get_license_key(&email) + .map(|_| println!("A license key was sent to your e-mail address.")) + .map_err(exceptions::PyConnectionError::new_err) } /// Specifies the type of the atom. diff --git a/src/atom.rs b/src/atom.rs index 3ac0becc..703935d1 100644 --- a/src/atom.rs +++ b/src/atom.rs @@ -1,6 +1,8 @@ mod coefficient; pub mod representation; +use representation::{InlineNum, InlineVar}; + use crate::{ coefficient::Coefficient, parser::Token, @@ -203,6 +205,18 @@ impl<'a> AsAtomView<'a> for AtomView<'a> { } } +impl<'a> AsAtomView<'a> for &'a InlineVar { + fn as_atom_view(self) -> AtomView<'a> { + self.as_view() + } +} + +impl<'a> AsAtomView<'a> for &'a InlineNum { + fn as_atom_view(self) -> AtomView<'a> { + self.as_view() + } +} + impl<'a, T: AsRef> AsAtomView<'a> for &'a T { fn as_atom_view(self) -> AtomView<'a> { self.as_ref().as_view() diff --git a/src/atom/representation.rs b/src/atom/representation.rs index a33f79f7..8d8642f0 100644 --- a/src/atom/representation.rs +++ b/src/atom/representation.rs @@ -37,6 +37,98 @@ const ZERO_DATA: [u8; 3] = [NUM_ID, 1, 0]; pub type RawAtom = Vec; +/// An inline variable. +pub struct InlineVar { + data: [u8; 16], + size: u8, +} + +impl InlineVar { + /// Create a new inline variable. + pub fn new(symbol: Symbol) -> InlineVar { + let mut data = [0; 16]; + let mut flags = VAR_ID; + match symbol.wildcard_level { + 0 => {} + 1 => flags |= VAR_WILDCARD_LEVEL_1, + 2 => flags |= VAR_WILDCARD_LEVEL_2, + _ => flags |= VAR_WILDCARD_LEVEL_3, + } + + if symbol.is_symmetric { + flags |= FUN_SYMMETRIC_FLAG; + } + if symbol.is_linear { + flags |= FUN_LINEAR_FLAG; + } + if symbol.is_antisymmetric { + flags |= VAR_ANTISYMMETRIC_FLAG; + } + + data[0] = flags; + + let size = 1 + (symbol.id as u64, 1).get_packed_size() as u8; + (symbol.id as u64, 1).write_packed_fixed(&mut data[1..]); + InlineVar { data, size } + } + + pub fn get_data(&self) -> &[u8] { + &self.data[..self.size as usize] + } + + pub fn as_var_view(&self) -> VarView { + VarView { + data: &self.data[..self.size as usize], + } + } + + pub fn as_view(&self) -> AtomView { + AtomView::Var(VarView { + data: &self.data[..self.size as usize], + }) + } +} + +impl From for InlineVar { + fn from(symbol: Symbol) -> InlineVar { + InlineVar::new(symbol) + } +} + +/// An inline rational number that has 64-bit components. +pub struct InlineNum { + data: [u8; 24], + size: u8, +} + +impl InlineNum { + /// Create a new inline number. The gcd of num and den should be 1. + pub fn new(num: i64, den: i64) -> InlineNum { + let mut data = [0; 24]; + data[0] = NUM_ID; + + let size = 1 + (num, den).get_packed_size() as u8; + (num, den).write_packed_fixed(&mut data[1..]); + InlineNum { data, size } + } + + pub fn get_data(&self) -> &[u8] { + &self.data[..self.size as usize] + } + + pub fn as_num_view(&self) -> NumView { + NumView { + data: &self.data[..self.size as usize], + } + } + + pub fn as_view(&self) -> AtomView { + AtomView::Num(NumView { + data: &self.data[..self.size as usize], + }) + } +} + impl Atom { /// Read from a binary stream. The format is the byte-length first /// followed by the data. diff --git a/src/lib.rs b/src/lib.rs index 9aa8d5fe..6b6482e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -112,36 +112,41 @@ impl LicenseManager { }; } Err(e) => { - eprintln!("{}", e); + if !e.contains("missing") { + eprintln!("{}", e); + } } } - println!( - "┌────────────────────────────────────────────────────────┐ -│ You are running an unlicensed Symbolica instance. │ + if env::var("SYMBOLICA_HIDE_BANNER").is_err() { + println!( + "┌────────────────────────────────────────────────────────┐ +│ You are running a restricted Symbolica instance. │ │ │ -│ This mode is only allowed for non-professional use and │ -│ is limited to one instance and core. │ +│ This mode is only permitted for non-commercial use and │ +│ is limited to one instance and core per machine. │ │ │ -│ {} can easily acquire a free license to unlock │ -│ all cores and to remove this banner: │ +│ {} can easily acquire a {} license key │ +│ that unlocks all cores and removes this banner: │ │ │ │ from symbolica import * │ │ request_hobbyist_license('YOUR_NAME', 'YOUR_EMAIL') │ │ │ -│ {} users must obtain an appropriate license, │ -│ or can get a free 30-day trial license: │ +│ All other users can obtain a free 30-day trial key: │ │ │ │ from symbolica import * │ │ request_trial_license('NAME', 'EMAIL', 'EMPLOYER') │ │ │ │ See https://symbolica.io/docs/get_started.html#license │ └────────────────────────────────────────────────────────┘", - "Hobbyists".bold(), - "Professional".bold(), - ); + "Hobbyists".bold(), + "free".bold(), + ); + } + + let port = env::var("SYMBOLICA_PORT").unwrap_or_else(|_| "12011".to_owned()); - match TcpListener::bind("127.0.0.1:12011") { + match TcpListener::bind(&format!("127.0.0.1:{}", port)) { Ok(o) => { rayon::ThreadPoolBuilder::new() .num_threads(1) @@ -150,8 +155,16 @@ impl LicenseManager { drop(o); - std::thread::spawn(|| loop { - match TcpListener::bind("127.0.0.1:12011") { + std::thread::spawn(move || loop { + let new_port = + env::var("SYMBOLICA_PORT").unwrap_or_else(|_| "12011".to_owned()); + + if port != new_port { + println!("{}", MULTIPLE_INSTANCE_WARNING); + abort(); + } + + match TcpListener::bind(&format!("127.0.0.1:{}", port)) { Ok(_) => { std::thread::sleep(Duration::from_secs(1)); } @@ -183,14 +196,28 @@ impl LicenseManager { .cloned() .or(env::var("SYMBOLICA_LICENSE").ok()); - let Some(key) = key else { + let Some(mut key) = key else { + std::thread::spawn(|| { + let mut m: HashMap = HashMap::default(); + m.insert( + "version".to_owned(), + env!("CARGO_PKG_VERSION").to_owned().into(), + ); + let mut v = JsonValue::from(m).stringify().unwrap(); + v.push('\n'); + + if let Ok(mut stream) = TcpStream::connect(&"symbolica.io:12012") { + let _ = stream.write_all(v.as_bytes()); + }; + }); + return Err(MISSING_LICENSE_ERROR.to_owned()); }; - if key.contains('@') { - let mut a = key.split('@'); - let f1 = a.next().unwrap(); - let f2 = a.next().unwrap(); + if key.contains('#') { + let mut a = key.split('#'); + let f1 = a.next().ok_or_else(|| ACTIVATION_ERROR.to_owned())?; + let f2 = a.next().ok_or_else(|| ACTIVATION_ERROR.to_owned())?; let f3 = a.next().ok_or_else(|| ACTIVATION_ERROR.to_owned())?; let mut h: u32 = 5381; @@ -201,6 +228,7 @@ impl LicenseManager { h = h.wrapping_mul(33).wrapping_add(*b as u32); } + let h = format!("{:x}", h); if f1 != h.to_string() { Err(ACTIVATION_ERROR.to_owned())?; } @@ -210,20 +238,39 @@ impl LicenseManager { .unwrap() .as_secs(); - let t2 = f2.parse::().map_err(|_| ACTIVATION_ERROR.to_owned())?; + let t2 = u64::from_str_radix(f2, 16) + .map_err(|_| ACTIVATION_ERROR.to_owned()) + .unwrap(); - if t < t2 || t - t2 > 24 * 60 * 60 { - Err("┌───────────────────────────────────────────┐ -│ The offline Symbolica license has expired │ -└───────────────────────────────────────────┘" + if t > t2 { + Err("┌───────────────────────────────────┐ +│ The Symbolica license has expired │ +└───────────────────────────────────┘" .to_owned())?; } - return Ok(()); + key = f3.to_owned(); + std::thread::spawn(|| { + if let Err(e) = Self::check_registration(key) { + if e.contains("expired") { + println!("{}", e); + abort(); + } + } + }); + + Ok(()) + } else { + Self::check_registration(key) } + } - let Ok(mut stream) = TcpStream::connect("symbolica.io:12012") else { - return Err(NETWORK_ERROR.to_owned()); + fn check_registration(key: String) -> Result<(), String> { + let mut stream = match TcpStream::connect("symbolica.io:12012") { + Ok(stream) => stream, + Err(_) => { + return Err(NETWORK_ERROR.to_owned()); + } }; let mut m: HashMap = HashMap::default(); @@ -412,38 +459,32 @@ Error: {}", } } - /// Get a license key for offline use, generated from a licensed Symbolica session. The key will remain valid for 24 hours. - pub fn get_offline_license_key() -> Result { - if Self::check_license_key().is_err() { - Err("Cannot request offline license from an unlicensed session".to_owned())?; - } - let key = LICENSE_KEY - .get() - .cloned() - .or(env::var("SYMBOLICA_LICENSE").ok()); + /// Get the license key for the account registered with the provided email address. + pub fn get_license_key(email: &str) -> Result<(), String> { + if let Ok(mut stream) = TcpStream::connect("symbolica.io:12012") { + let mut m: HashMap = HashMap::default(); + m.insert("email".to_owned(), email.to_owned().into()); + let mut v = JsonValue::from(m).stringify().unwrap(); + v.push('\n'); - let Some(key) = key else { - return Err(ACTIVATION_ERROR.to_owned()); - }; + stream.write_all(v.as_bytes()).unwrap(); - if !key.contains('@') { - let t = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs() - .to_string(); + let mut buf = Vec::new(); + stream.read_to_end(&mut buf).unwrap(); + let read_str = std::str::from_utf8(&buf).unwrap(); - let mut h: u32 = 5381; - for b in t.as_bytes() { - h = h.wrapping_mul(33).wrapping_add(*b as u32); - } - for b in key.as_bytes() { - h = h.wrapping_mul(33).wrapping_add(*b as u32); + if read_str == "{\"status\":\"email sent\"}\n" { + Ok(()) + } else if read_str.is_empty() { + Err("Empty response".to_owned()) + } else { + let message: JsonValue = read_str[..read_str.len() - 1].parse().unwrap(); + let message_parsed: &HashMap<_, _> = message.get().unwrap(); + let status: &String = message_parsed.get("status").unwrap().get().unwrap(); + Err(status.clone()) } - - Ok(format!("{}@{}@{}", h, t, key)) } else { - Err("Cannot request offline license key from an offline session".to_owned()) + Err("Could not connect to the license server".to_owned()) } } } diff --git a/src/state.rs b/src/state.rs index 3f585450..866badec 100644 --- a/src/state.rs +++ b/src/state.rs @@ -575,7 +575,11 @@ impl Workspace { /// Get a thread-local workspace. #[inline] - pub const fn get_local() -> &'static LocalKey> { + pub fn get_local() -> &'static LocalKey> { + if let Some(l) = LICENSE_MANAGER.get() { + l.check(); + } + &WORKSPACE } diff --git a/src/streaming.rs b/src/streaming.rs index 59bc75f2..5e896312 100644 --- a/src/streaming.rs +++ b/src/streaming.rs @@ -12,6 +12,7 @@ use rayon::prelude::*; use crate::{ atom::{Atom, AtomView}, state::RecycledAtom, + LicenseManager, }; pub trait ReadableNamedStream: Read + Send { @@ -185,7 +186,11 @@ impl TermStreamer { filename, thread_pool: Arc::new( rayon::ThreadPoolBuilder::new() - .num_threads(config.n_cores) + .num_threads(if LicenseManager::is_licensed() { + config.n_cores + } else { + 1 + }) .build() .unwrap(), ), @@ -439,6 +444,10 @@ impl TermStreamer { /// Map every term in the stream using the function `f`. The resulting terms /// are a stream as well, which is returned by this function. pub fn map(&mut self, f: impl Fn(Atom) -> Atom + Send + Sync) -> Self { + if self.thread_pool.current_num_threads() == 1 { + return self.map_single_thread(f); + } + let t = self.thread_pool.clone(); let new_out = self.next_generation();