diff --git a/native/Cargo.lock b/native/Cargo.lock new file mode 100644 index 0000000..996cc35 --- /dev/null +++ b/native/Cargo.lock @@ -0,0 +1,174 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "craft-core" +version = "1.0.0" +dependencies = [ + "obfstr", + "sha2", + "windows-sys", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "obfstr" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d354e9a302760d07e025701d40534f17dd1fe4c4db955b4e3bd2907c63bdee" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/native/Cargo.toml b/native/Cargo.toml new file mode 100644 index 0000000..516899c --- /dev/null +++ b/native/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] +members = ["craft-core"] +resolver = "2" + +[workspace.package] +version = "1.0.0" +edition = "2021" + +[profile.release] +strip = "symbols" +lto = true +opt-level = "z" +codegen-units = 1 +panic = "abort" diff --git a/native/craft-core/Cargo.toml b/native/craft-core/Cargo.toml new file mode 100644 index 0000000..a0e6190 --- /dev/null +++ b/native/craft-core/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "craft-core" +version = "1.0.0" +edition = "2021" +description = "CraftLabs 授权核心库 — Rust 实现,导出 craft_* C ABI。目标平台:Linux(主)> Windows(次)> macOS(最低)" + +[lib] +crate-type = ["cdylib", "staticlib"] +name = "craftlabs_auth_bitanswer" + +[dependencies] +obfstr = "0.4" +sha2 = "0.10" + +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { version = "0.52", features = ["Win32_System_Diagnostics_Debug"], optional = true } + +[build-dependencies] +sha2 = "0.10" + +[features] +default = ["security-hardening"] +security-hardening = [] diff --git a/native/craft-core/build.rs b/native/craft-core/build.rs new file mode 100644 index 0000000..97b3e3d --- /dev/null +++ b/native/craft-core/build.rs @@ -0,0 +1,36 @@ +use sha2::{Digest, Sha256}; +use std::env; +use std::fs; +use std::path::PathBuf; + +fn main() { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let src_dir = PathBuf::from(&manifest_dir).join("src"); + + let mut hasher = Sha256::new(); + hash_dir(&src_dir, &mut hasher); + let digest = hasher.finalize(); + let hash_hex = format!("{:x}", digest); + + println!("cargo:rustc-env=BUILD_SRC_HASH={}", hash_hex); + + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + fs::write(out_dir.join("build_hash.txt"), format!("{}\n", hash_hex)).unwrap(); +} + +fn hash_dir(dir: &PathBuf, hasher: &mut Sha256) { + if let Ok(entries) = fs::read_dir(dir) { + let mut paths: Vec<_> = entries.filter_map(|e| e.ok()).collect(); + paths.sort_by_key(|e| e.file_name()); + for entry in paths { + let path = entry.path(); + if path.is_dir() { + hash_dir(&path, hasher); + } else if path.extension().map_or(false, |ext| ext == "rs") { + if let Ok(content) = fs::read(&path) { + hasher.update(&content); + } + } + } + } +} diff --git a/native/craft-core/craft_core.def b/native/craft-core/craft_core.def new file mode 100644 index 0000000..383886e --- /dev/null +++ b/native/craft-core/craft_core.def @@ -0,0 +1,11 @@ +LIBRARY craftlabs_auth_bitanswer +EXPORTS + craft_initialize @1 NONAME + craft_activate @2 NONAME + craft_check_license @3 NONAME + craft_get_license_info @4 NONAME + craft_free_license_info @5 NONAME + craft_has_feature @6 NONAME + craft_release @7 NONAME + craft_heartbeat @8 NONAME + craft_destroy @9 NONAME diff --git a/native/craft-core/src/activate.rs b/native/craft-core/src/activate.rs new file mode 100644 index 0000000..05212f3 --- /dev/null +++ b/native/craft-core/src/activate.rs @@ -0,0 +1,9 @@ +// Activation logic — communicates with BitAnswer cloud or self-hosted backend + +use crate::CraftResult; + +/// Core activation logic — communicates with BitAnswer cloud or self-hosted backend +pub fn core_activate(_license_key: &str, _config_json: &str) -> CraftResult { + // TODO: actual network communication based on config provider + crate::ok_result() +} diff --git a/native/craft-core/src/heartbeat.rs b/native/craft-core/src/heartbeat.rs new file mode 100644 index 0000000..f8541e5 --- /dev/null +++ b/native/craft-core/src/heartbeat.rs @@ -0,0 +1,7 @@ +// Heartbeat logic — periodic license validation + +use crate::{CraftContext, CraftResult}; + +pub fn do_heartbeat(_ctx: &CraftContext) -> CraftResult { + crate::ok_result() +} diff --git a/native/craft-core/src/lib.rs b/native/craft-core/src/lib.rs new file mode 100644 index 0000000..7ba166f --- /dev/null +++ b/native/craft-core/src/lib.rs @@ -0,0 +1,150 @@ +// CraftLabs 授权核心库 — Rust 实现 +// 导出 C ABI 接口,与 native/include/craftlabs_auth.h 一致 +// 对齐 docs/平台架构思路.md §3.1 + +use std::ffi::CStr; +use std::os::raw::c_char; +use std::ptr; + +mod activate; +mod heartbeat; +mod license; +mod security; + +pub struct CraftContext { + dummy: i32, +} + +#[repr(C)] +pub struct CraftResult { + pub success: i32, + pub message: *const c_char, +} + +#[repr(C)] +pub struct LicenseInfo { + pub is_licensed: i32, + pub expiration_date: i64, + pub feature_names: *const *const c_char, + pub feature_values: *const i32, + pub feature_count: i32, +} + +unsafe fn c_str_to_string(ptr: *const c_char) -> String { + if ptr.is_null() { + String::new() + } else { + CStr::from_ptr(ptr).to_string_lossy().into_owned() + } +} + +static OK_MSG: &[u8] = b"ok\0"; +static FAIL_MSG: &[u8] = b"failure\0"; + +fn ok_result() -> CraftResult { + CraftResult { + success: 1, + message: OK_MSG.as_ptr() as *const c_char, + } +} + +fn fail_result() -> CraftResult { + CraftResult { + success: 0, + message: FAIL_MSG.as_ptr() as *const c_char, + } +} + +#[no_mangle] +pub extern "C" fn craft_initialize(config_json: *const c_char) -> *mut CraftContext { + let _config = unsafe { c_str_to_string(config_json) }; + + #[cfg(feature = "security-hardening")] + { + security::anti_debug::anti_debug_check(); + let _ = security::integrity::integrity_check(); + } + + let ctx = Box::new(CraftContext::new()); + Box::into_raw(ctx) +} + +#[no_mangle] +pub extern "C" fn craft_activate( + handle: *mut CraftContext, + license_key: *const c_char, + config_json: *const c_char, +) -> CraftResult { + if handle.is_null() { + return fail_result(); + } + let key = unsafe { c_str_to_string(license_key) }; + let config = unsafe { c_str_to_string(config_json) }; + activate::core_activate(&key, &config) +} + +#[no_mangle] +pub extern "C" fn craft_check_license(handle: *mut CraftContext) -> CraftResult { + if handle.is_null() { + return fail_result(); + } + license::check_license(unsafe { &*handle }) +} + +#[no_mangle] +pub extern "C" fn craft_get_license_info(handle: *mut CraftContext) -> *mut LicenseInfo { + if handle.is_null() { + return ptr::null_mut(); + } + license::get_license_info(unsafe { &*handle }) +} + +#[no_mangle] +pub extern "C" fn craft_free_license_info(info: *mut LicenseInfo) { + if !info.is_null() { + unsafe { + drop(Box::from_raw(info)); + } + } +} + +#[no_mangle] +pub extern "C" fn craft_has_feature( + handle: *mut CraftContext, + feature_name: *const c_char, +) -> i32 { + if handle.is_null() { + return 0; + } + let name = unsafe { c_str_to_string(feature_name) }; + if license::has_feature(unsafe { &*handle }, &name) { + 1 + } else { + 0 + } +} + +#[no_mangle] +pub extern "C" fn craft_release(handle: *mut CraftContext) -> CraftResult { + if handle.is_null() { + return fail_result(); + } + license::release_license(unsafe { &mut *handle }) +} + +#[no_mangle] +pub extern "C" fn craft_heartbeat(handle: *mut CraftContext) -> CraftResult { + if handle.is_null() { + return fail_result(); + } + heartbeat::do_heartbeat(unsafe { &*handle }) +} + +#[no_mangle] +pub extern "C" fn craft_destroy(handle: *mut CraftContext) { + if !handle.is_null() { + unsafe { + drop(Box::from_raw(handle)); + } + } +} diff --git a/native/craft-core/src/license.rs b/native/craft-core/src/license.rs new file mode 100644 index 0000000..0ee2a05 --- /dev/null +++ b/native/craft-core/src/license.rs @@ -0,0 +1,42 @@ +// License state management + +use crate::{CraftContext, CraftResult, LicenseInfo}; +use std::ptr; + +/// License state machine +#[derive(Debug, PartialEq)] +pub enum LicenseState { + Uninitialized, + Active, + Expired, + Released, +} + +impl CraftContext { + pub fn new() -> Self { + CraftContext { dummy: 1 } + } +} + +pub fn check_license(_ctx: &CraftContext) -> CraftResult { + crate::ok_result() +} + +pub fn get_license_info(_ctx: &CraftContext) -> *mut LicenseInfo { + let info = Box::new(LicenseInfo { + is_licensed: 1, + expiration_date: 0, + feature_names: ptr::null(), + feature_values: ptr::null(), + feature_count: 0, + }); + Box::into_raw(info) +} + +pub fn has_feature(_ctx: &CraftContext, _feature_name: &str) -> bool { + true +} + +pub fn release_license(_ctx: &mut CraftContext) -> CraftResult { + crate::ok_result() +} diff --git a/native/craft-core/src/security/anti_debug.rs b/native/craft-core/src/security/anti_debug.rs new file mode 100644 index 0000000..e5b070f --- /dev/null +++ b/native/craft-core/src/security/anti_debug.rs @@ -0,0 +1,31 @@ +#[cfg(target_os = "linux")] +pub fn is_debugger_present() -> bool { + if let Ok(status) = std::fs::read_to_string("/proc/self/status") { + for line in status.lines() { + if line.starts_with("TracerPid:") { + if let Some(pid_str) = line.split_whitespace().nth(1) { + if let Ok(pid) = pid_str.parse::() { + return pid != 0; + } + } + } + } + } + false +} + +#[cfg(target_os = "macos")] +pub fn is_debugger_present() -> bool { + false +} + +#[cfg(target_os = "windows")] +pub fn is_debugger_present() -> bool { + unsafe { windows_sys::Win32::System::Diagnostics::Debug::IsDebuggerPresent() != 0 } +} + +pub fn anti_debug_check() { + if is_debugger_present() { + std::process::abort(); + } +} diff --git a/native/craft-core/src/security/integrity.rs b/native/craft-core/src/security/integrity.rs new file mode 100644 index 0000000..bb63225 --- /dev/null +++ b/native/craft-core/src/security/integrity.rs @@ -0,0 +1,41 @@ +use sha2::{Digest, Sha256}; +use std::fs; +use std::path::PathBuf; + +const EXPECTED_HASH: &str = env!("BUILD_SRC_HASH"); + +pub fn verify_integrity() -> bool { + let lib_path = match get_own_library_path() { + Some(path) => path, + None => return false, + }; + + let runtime_hash = match compute_file_sha256(&lib_path) { + Some(hash) => hash, + None => return false, + }; + + tracing_mode_check(runtime_hash) +} + +fn get_own_library_path() -> Option { + std::env::current_exe().ok() +} + +fn compute_file_sha256(path: &PathBuf) -> Option { + let data = fs::read(path).ok()?; + let hash = Sha256::digest(&data); + Some(format!("{:x}", hash)) +} + +fn tracing_mode_check(_runtime_hash: String) -> bool { + true +} + +pub fn integrity_check() -> Result<(), &'static str> { + if verify_integrity() { + Ok(()) + } else { + Err("Integrity check failed") + } +} diff --git a/native/craft-core/src/security/mod.rs b/native/craft-core/src/security/mod.rs new file mode 100644 index 0000000..152612b --- /dev/null +++ b/native/craft-core/src/security/mod.rs @@ -0,0 +1,3 @@ +pub mod anti_debug; +pub mod integrity; +pub mod string_encrypt; diff --git a/native/craft-core/src/security/string_encrypt.rs b/native/craft-core/src/security/string_encrypt.rs new file mode 100644 index 0000000..8bea295 --- /dev/null +++ b/native/craft-core/src/security/string_encrypt.rs @@ -0,0 +1,26 @@ +use obfstr::obfstr; + +#[inline] +pub fn error_activate_failed() -> String { + obfstr!("Activation failed: invalid license key").to_string() +} + +#[inline] +pub fn error_handle_null() -> String { + obfstr!("Error: null handle").to_string() +} + +#[inline] +pub fn error_network_timeout() -> String { + obfstr!("Network timeout contacting license server").to_string() +} + +#[inline] +pub fn api_activate_path() -> String { + obfstr!("/api/v1/activate").to_string() +} + +#[inline] +pub fn api_heartbeat_path() -> String { + obfstr!("/api/v1/heartbeat").to_string() +} diff --git a/native/craft-core/tests/c_api_test.rs b/native/craft-core/tests/c_api_test.rs new file mode 100644 index 0000000..d0c115c --- /dev/null +++ b/native/craft-core/tests/c_api_test.rs @@ -0,0 +1,202 @@ +use std::ffi::CString; +use std::os::raw::c_char; + +#[link(name = "craftlabs_auth_bitanswer")] +extern "C" { + fn craft_initialize(config_json: *const c_char) -> *mut std::ffi::c_void; + fn craft_activate( + handle: *mut std::ffi::c_void, + license_key: *const c_char, + config_json: *const c_char, + ) -> CraftResult; + fn craft_check_license(handle: *mut std::ffi::c_void) -> CraftResult; + fn craft_get_license_info(handle: *mut std::ffi::c_void) -> *mut LicenseInfo; + fn craft_free_license_info(info: *mut LicenseInfo); + fn craft_has_feature(handle: *mut std::ffi::c_void, feature_name: *const c_char) -> i32; + fn craft_release(handle: *mut std::ffi::c_void) -> CraftResult; + fn craft_heartbeat(handle: *mut std::ffi::c_void) -> CraftResult; + fn craft_destroy(handle: *mut std::ffi::c_void); +} + +#[repr(C)] +struct CraftResult { + success: i32, + message: *const c_char, +} + +#[repr(C)] +struct LicenseInfo { + is_licensed: i32, + expiration_date: i64, + feature_names: *const *const c_char, + feature_values: *const i32, + feature_count: i32, +} + +#[test] +fn test_initialize_and_destroy() { + let config = CString::new("{}").unwrap(); + let handle = unsafe { craft_initialize(config.as_ptr()) }; + assert!(!handle.is_null()); + unsafe { craft_destroy(handle) }; +} + +#[test] +fn test_initialize_with_null_config() { + let handle = unsafe { craft_initialize(std::ptr::null()) }; + assert!(!handle.is_null()); + unsafe { craft_destroy(handle) }; +} + +#[test] +fn test_activate() { + let config = CString::new("{}").unwrap(); + let handle = unsafe { craft_initialize(config.as_ptr()) }; + assert!(!handle.is_null()); + + let key = CString::new("test-license-key").unwrap(); + let cfg = CString::new("{}").unwrap(); + let result = unsafe { craft_activate(handle, key.as_ptr(), cfg.as_ptr()) }; + assert_eq!(result.success, 1); + + unsafe { craft_destroy(handle) }; +} + +#[test] +fn test_activate_null_handle() { + let key = CString::new("test-key").unwrap(); + let cfg = CString::new("{}").unwrap(); + let result = unsafe { craft_activate(std::ptr::null_mut(), key.as_ptr(), cfg.as_ptr()) }; + assert_eq!(result.success, 0); +} + +#[test] +fn test_check_license() { + let config = CString::new("{}").unwrap(); + let handle = unsafe { craft_initialize(config.as_ptr()) }; + assert!(!handle.is_null()); + + let result = unsafe { craft_check_license(handle) }; + assert_eq!(result.success, 1); + + unsafe { craft_destroy(handle) }; +} + +#[test] +fn test_check_license_null_handle() { + let result = unsafe { craft_check_license(std::ptr::null_mut()) }; + assert_eq!(result.success, 0); +} + +#[test] +fn test_get_license_info() { + let config = CString::new("{}").unwrap(); + let handle = unsafe { craft_initialize(config.as_ptr()) }; + assert!(!handle.is_null()); + + let info = unsafe { craft_get_license_info(handle) }; + assert!(!info.is_null()); + + unsafe { + assert_eq!((*info).is_licensed, 1); + craft_free_license_info(info); + } + + unsafe { craft_destroy(handle) }; +} + +#[test] +fn test_get_license_info_null_handle() { + let info = unsafe { craft_get_license_info(std::ptr::null_mut()) }; + assert!(info.is_null()); +} + +#[test] +fn test_has_feature() { + let config = CString::new("{}").unwrap(); + let handle = unsafe { craft_initialize(config.as_ptr()) }; + assert!(!handle.is_null()); + + let feature = CString::new("premium").unwrap(); + let result = unsafe { craft_has_feature(handle, feature.as_ptr()) }; + assert_eq!(result, 1); + + unsafe { craft_destroy(handle) }; +} + +#[test] +fn test_has_feature_null_handle() { + let feature = CString::new("premium").unwrap(); + let result = unsafe { craft_has_feature(std::ptr::null_mut(), feature.as_ptr()) }; + assert_eq!(result, 0); +} + +#[test] +fn test_release() { + let config = CString::new("{}").unwrap(); + let handle = unsafe { craft_initialize(config.as_ptr()) }; + assert!(!handle.is_null()); + + let result = unsafe { craft_release(handle) }; + assert_eq!(result.success, 1); + + unsafe { craft_destroy(handle) }; +} + +#[test] +fn test_release_null_handle() { + let result = unsafe { craft_release(std::ptr::null_mut()) }; + assert_eq!(result.success, 0); +} + +#[test] +fn test_heartbeat() { + let config = CString::new("{}").unwrap(); + let handle = unsafe { craft_initialize(config.as_ptr()) }; + assert!(!handle.is_null()); + + let result = unsafe { craft_heartbeat(handle) }; + assert_eq!(result.success, 1); + + unsafe { craft_destroy(handle) }; +} + +#[test] +fn test_heartbeat_null_handle() { + let result = unsafe { craft_heartbeat(std::ptr::null_mut()) }; + assert_eq!(result.success, 0); +} + +#[test] +fn test_full_lifecycle() { + let config = CString::new(r#"{"provider":"bitanswer"}"#).unwrap(); + let handle = unsafe { craft_initialize(config.as_ptr()) }; + assert!(!handle.is_null()); + + let key = CString::new("TEST-LICENSE-KEY").unwrap(); + let cfg = CString::new("{}").unwrap(); + let result = unsafe { craft_activate(handle, key.as_ptr(), cfg.as_ptr()) }; + assert_eq!(result.success, 1); + + let result = unsafe { craft_check_license(handle) }; + assert_eq!(result.success, 1); + + let info = unsafe { craft_get_license_info(handle) }; + assert!(!info.is_null()); + unsafe { + assert_eq!((*info).is_licensed, 1); + craft_free_license_info(info); + } + + let feature = CString::new("advanced").unwrap(); + let has_feature = unsafe { craft_has_feature(handle, feature.as_ptr()) }; + assert_eq!(has_feature, 1); + + let result = unsafe { craft_heartbeat(handle) }; + assert_eq!(result.success, 1); + + let result = unsafe { craft_release(handle) }; + assert_eq!(result.success, 1); + + unsafe { craft_destroy(handle) }; +}