use atproto_client::{HttpRecordResolver, RecordResolver}; use atproto_identity::resolve::{HickoryDnsResolver, InnerIdentityResolver, SharedIdentityResolver}; use atproto_record::aturi::ATURI; use nickel_lang_core::deserialize; use std::ffi::{CStr, CString}; use std::io::Read; use std::os::raw::c_char; use std::ptr; use std::str::FromStr; use std::sync::Arc; fn render_json_value(value: &serde_json::Value) -> Result { match value { serde_json::Value::Null => Ok("null".to_owned()), serde_json::Value::Bool(b) => Ok(if *b { "true" } else { "false" }.to_owned()), serde_json::Value::Number(n) => { if let Some(i) = n.as_i64() { Ok(i.to_string()) } else if let Some(u) = n.as_u64() { Ok(u.to_string()) } else { Ok(n.to_string()) } } serde_json::Value::String(s) => serde_json::to_string(s) .map_err(|err| format!("failed to render Nickel string: {err}")), serde_json::Value::Array(items) => { let rendered = items .iter() .map(render_json_value) .collect::, _>>()?; Ok(format!("[{}]", rendered.join(", "))) } serde_json::Value::Object(fields) => { let rendered = fields .iter() .map(|(key, value)| { let key = if key .chars() .next() .map(|c| c.is_ascii_alphabetic() || c == '_') .unwrap_or(false) && key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') { key.clone() } else { serde_json::to_string(key) .map_err(|err| format!("failed to render Nickel key: {err}"))? }; Ok(format!("{key} = {}", render_json_value(value)?)) }) .collect::, String>>()?; Ok(format!("{{ {} }}", rendered.join(", "))) } } } fn nickel_to_data_string(source: &str) -> Result { let value: serde_json::Value = deserialize::from_str(source).map_err(|err| format!("{err}"))?; render_json_value(&value) } fn nickel_file_to_data_string(path: &str) -> Result { let value: serde_json::Value = deserialize::from_path(path).map_err(|err| format!("{err}"))?; render_json_value(&value) } fn json_to_data_string(source: &str) -> Result { let value: serde_json::Value = serde_json::from_str(source).map_err(|err| format!("{err}"))?; render_json_value(&value) } fn aturi_to_data_string(source: &str) -> Result { ATURI::from_str(source).map_err(|err| format!("{err}"))?; let runtime = tokio::runtime::Runtime::new() .map_err(|err| format!("failed to create Tokio runtime: {err}"))?; let value: serde_json::Value = runtime .block_on(async { let http_client = reqwest::Client::new(); let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[])); let identity_resolver = SharedIdentityResolver(Arc::new(InnerIdentityResolver { dns_resolver, http_client: http_client.clone(), plc_hostname: "plc.directory".to_owned(), })); let resolver = HttpRecordResolver::new(http_client, Arc::new(identity_resolver)); resolver.resolve(source).await }) .map_err(|err| format!("AT-URI lookup failed: {err}"))?; let body = serde_json::to_string(&value).map_err(|err| format!("failed to encode record: {err}"))?; json_to_data_string(&body) } fn alloc_c_string(s: String) -> *mut c_char { CString::new(s) .unwrap_or_else(|_| CString::new("string contained interior NUL").unwrap()) .into_raw() } #[no_mangle] pub extern "C" fn mlang_http_get_body( url: *const c_char, out_body: *mut *mut c_char, out_error: *mut *mut c_char, ) -> i32 { if url.is_null() || out_body.is_null() || out_error.is_null() { return 0; } unsafe { *out_body = ptr::null_mut(); *out_error = ptr::null_mut(); } let url = unsafe { CStr::from_ptr(url) }; let url = match url.to_str() { Ok(url) => url, Err(err) => { unsafe { *out_error = alloc_c_string(format!("invalid URL UTF-8: {err}")); } return 0; } }; let response = match ureq::get(url).call() { Ok(response) => response, Err(err) => { unsafe { *out_error = alloc_c_string(format!("http request failed: {err}")); } return 0; } }; let mut reader = response.into_reader(); let mut body = String::new(); if let Err(err) = reader.read_to_string(&mut body) { unsafe { *out_error = alloc_c_string(format!("failed to read HTTP response body: {err}")); } return 0; } unsafe { *out_body = alloc_c_string(body); } 1 } #[no_mangle] pub extern "C" fn mlang_http_string_free(ptr: *mut c_char) { if ptr.is_null() { return; } unsafe { drop(CString::from_raw(ptr)); } } #[no_mangle] pub extern "C" fn mlang_nickel_eval( source: *const c_char, out_body: *mut *mut c_char, out_error: *mut *mut c_char, ) -> i32 { if source.is_null() || out_body.is_null() || out_error.is_null() { return 0; } unsafe { *out_body = ptr::null_mut(); *out_error = ptr::null_mut(); } let source = unsafe { CStr::from_ptr(source) }; let source = match source.to_str() { Ok(source) => source, Err(err) => { unsafe { *out_error = alloc_c_string(format!("invalid Nickel source UTF-8: {err}")); } return 0; } }; match nickel_to_data_string(source) { Ok(rendered) => { unsafe { *out_body = alloc_c_string(rendered); } 1 } Err(err) => { unsafe { *out_error = alloc_c_string(format!("Nickel parse error: {err}")); } 0 } } } #[no_mangle] pub extern "C" fn mlang_nickel_eval_file( path: *const c_char, out_body: *mut *mut c_char, out_error: *mut *mut c_char, ) -> i32 { if path.is_null() || out_body.is_null() || out_error.is_null() { return 0; } unsafe { *out_body = ptr::null_mut(); *out_error = ptr::null_mut(); } let path = unsafe { CStr::from_ptr(path) }; let path = match path.to_str() { Ok(path) => path, Err(err) => { unsafe { *out_error = alloc_c_string(format!("invalid Nickel file path UTF-8: {err}")); } return 0; } }; match nickel_file_to_data_string(path) { Ok(rendered) => { unsafe { *out_body = alloc_c_string(rendered); } 1 } Err(err) => { unsafe { *out_error = alloc_c_string(format!("Nickel parse error: {err}")); } 0 } } } #[no_mangle] pub extern "C" fn mlang_json_eval( source: *const c_char, out_body: *mut *mut c_char, out_error: *mut *mut c_char, ) -> i32 { if source.is_null() || out_body.is_null() || out_error.is_null() { return 0; } unsafe { *out_body = ptr::null_mut(); *out_error = ptr::null_mut(); } let source = unsafe { CStr::from_ptr(source) }; let source = match source.to_str() { Ok(source) => source, Err(err) => { unsafe { *out_error = alloc_c_string(format!("invalid JSON source UTF-8: {err}")); } return 0; } }; match json_to_data_string(source) { Ok(rendered) => { unsafe { *out_body = alloc_c_string(rendered); } 1 } Err(err) => { unsafe { *out_error = alloc_c_string(format!("JSON parse error: {err}")); } 0 } } } #[no_mangle] pub extern "C" fn mlang_aturi_parse( source: *const c_char, out_body: *mut *mut c_char, out_error: *mut *mut c_char, ) -> i32 { if source.is_null() || out_body.is_null() || out_error.is_null() { return 0; } unsafe { *out_body = ptr::null_mut(); *out_error = ptr::null_mut(); } let source = unsafe { CStr::from_ptr(source) }; let source = match source.to_str() { Ok(source) => source, Err(err) => { unsafe { *out_error = alloc_c_string(format!("invalid AT-URI UTF-8: {err}")); } return 0; } }; match aturi_to_data_string(source) { Ok(rendered) => { unsafe { *out_body = alloc_c_string(rendered); } 1 } Err(err) => { unsafe { *out_error = alloc_c_string(format!("AT-URI parse error: {err}")); } 0 } } }