use crate::{BindingCoreError, BindingResult, ErrorKind};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use jacs::simple::SimpleAgent;
use serde::Serialize;
use std::sync::Arc;
#[derive(Clone)]
pub struct SimpleAgentWrapper {
inner: Arc<SimpleAgent>,
}
const _: () = {
fn _assert<T: Send + Sync>() {}
let _ = _assert::<SimpleAgentWrapper>;
};
fn serialize_json<T: Serialize>(value: &T, context: &str) -> BindingResult<String> {
serde_json::to_string(value).map_err(|e| {
BindingCoreError::serialization_failed(format!("Failed to serialize {}: {}", context, e))
})
}
fn encode_base64(bytes: &[u8]) -> String {
STANDARD.encode(bytes)
}
fn decode_base64(input: &str, label: &str) -> BindingResult<Vec<u8>> {
STANDARD
.decode(input)
.map_err(|e| BindingCoreError::invalid_argument(format!("Invalid base64 {}: {}", label, e)))
}
fn conversion_error(operation: &str, err: impl std::fmt::Display) -> BindingCoreError {
BindingCoreError::new(
ErrorKind::SerializationFailed,
format!("{} failed: {}", operation, err),
)
}
impl SimpleAgentWrapper {
pub fn create(
name: &str,
purpose: Option<&str>,
key_algorithm: Option<&str>,
) -> BindingResult<(Self, String)> {
let (agent, info) = SimpleAgent::create(name, purpose, key_algorithm)
.map_err(|e| BindingCoreError::agent_load(format!("Failed to create agent: {}", e)))?;
let info_json = crate::serialize_agent_info(&info)?;
Ok((Self::from_agent(agent), info_json))
}
pub fn load(config_path: Option<&str>, strict: Option<bool>) -> BindingResult<Self> {
let (wrapper, _info_json) = Self::load_with_info(config_path, strict)?;
Ok(wrapper)
}
pub fn load_with_info(
config_path: Option<&str>,
strict: Option<bool>,
) -> BindingResult<(Self, String)> {
let requested_path = config_path.unwrap_or("./jacs.config.json");
let resolved_config_path = crate::resolve_existing_config_path(requested_path)?;
let agent = SimpleAgent::load(Some(&resolved_config_path), strict)
.map_err(|e| BindingCoreError::agent_load(format!("Failed to load agent: {}", e)))?;
let info = agent
.loaded_info()
.map_err(|e| BindingCoreError::agent_load(format!("Failed to load agent: {}", e)))?;
let info_json = crate::serialize_agent_info(&info)?;
Ok((Self::from_agent(agent), info_json))
}
pub fn ephemeral(algorithm: Option<&str>) -> BindingResult<(Self, String)> {
let (agent, info) = SimpleAgent::ephemeral(algorithm).map_err(|e| {
BindingCoreError::agent_load(format!("Failed to create ephemeral agent: {}", e))
})?;
let info_json = crate::serialize_agent_info(&info)?;
Ok((Self::from_agent(agent), info_json))
}
pub fn create_with_params(params_json: &str) -> BindingResult<(Self, String)> {
let params: jacs::simple::CreateAgentParams =
serde_json::from_str(params_json).map_err(|e| {
BindingCoreError::invalid_argument(format!("Invalid CreateAgentParams JSON: {}", e))
})?;
let (agent, info) = SimpleAgent::create_with_params(params).map_err(|e| {
BindingCoreError::agent_load(format!("Failed to create agent with params: {}", e))
})?;
let info_json = crate::serialize_agent_info(&info)?;
Ok((Self::from_agent(agent), info_json))
}
pub fn from_agent(agent: SimpleAgent) -> Self {
Self {
inner: Arc::new(agent),
}
}
pub fn inner_ref(&self) -> &SimpleAgent {
&self.inner
}
pub fn get_agent_id(&self) -> BindingResult<String> {
self.inner
.get_agent_id()
.map_err(|e| BindingCoreError::generic(format!("Failed to get agent ID: {}", e)))
}
pub fn key_id(&self) -> BindingResult<String> {
self.inner
.key_id()
.map_err(|e| BindingCoreError::generic(format!("Failed to get key ID: {}", e)))
}
pub fn is_strict(&self) -> bool {
self.inner.is_strict()
}
pub fn config_path(&self) -> Option<String> {
self.inner.config_path().map(|s| s.to_string())
}
pub fn export_agent(&self) -> BindingResult<String> {
self.inner
.export_agent()
.map_err(|e| BindingCoreError::generic(format!("Failed to export agent: {}", e)))
}
pub fn get_public_key_pem(&self) -> BindingResult<String> {
self.inner.get_public_key_pem().map_err(|e| {
BindingCoreError::key_not_found(format!("Failed to get public key PEM: {}", e))
})
}
pub fn get_public_key_base64(&self) -> BindingResult<String> {
let bytes = self.inner.get_public_key().map_err(|e| {
BindingCoreError::key_not_found(format!("Failed to get public key: {}", e))
})?;
Ok(encode_base64(&bytes))
}
pub fn diagnostics(&self) -> String {
self.inner.diagnostics().to_string()
}
pub fn verify_self(&self) -> BindingResult<String> {
let result = self.inner.verify_self().map_err(|e| {
BindingCoreError::verification_failed(format!("Verify self failed: {}", e))
})?;
serialize_json(&result, "VerificationResult")
}
pub fn verify_json(&self, signed_document: &str) -> BindingResult<String> {
let result = self.inner.verify(signed_document).map_err(|e| {
BindingCoreError::verification_failed(format!("Verification failed: {}", e))
})?;
serialize_json(&result, "VerificationResult")
}
pub fn verify_with_key_json(
&self,
signed_document: &str,
public_key_base64: &str,
) -> BindingResult<String> {
let key_bytes = decode_base64(public_key_base64, "public key")?;
let result = self
.inner
.verify_with_key(signed_document, key_bytes)
.map_err(|e| {
BindingCoreError::verification_failed(format!(
"Verification with key failed: {}",
e
))
})?;
serialize_json(&result, "VerificationResult")
}
pub fn verify_by_id_json(&self, document_id: &str) -> BindingResult<String> {
let result = self.inner.verify_by_id(document_id).map_err(|e| {
BindingCoreError::verification_failed(format!("Verify by ID failed: {}", e))
})?;
serialize_json(&result, "VerificationResult")
}
pub fn sign_message_json(&self, data_json: &str) -> BindingResult<String> {
let value: serde_json::Value = serde_json::from_str(data_json).map_err(|e| {
BindingCoreError::invalid_argument(format!("Invalid JSON input: {}", e))
})?;
let signed = self
.inner
.sign_message(&value)
.map_err(|e| BindingCoreError::signing_failed(format!("Sign message failed: {}", e)))?;
Ok(signed.raw)
}
pub fn sign_raw_bytes_base64(&self, data: &[u8]) -> BindingResult<String> {
let sig_bytes = self.inner.sign_raw_bytes(data).map_err(|e| {
BindingCoreError::signing_failed(format!("Sign raw bytes failed: {}", e))
})?;
Ok(encode_base64(&sig_bytes))
}
pub fn sign_file_json(&self, file_path: &str, embed: bool) -> BindingResult<String> {
let signed = self
.inner
.sign_file(file_path, embed)
.map_err(|e| BindingCoreError::signing_failed(format!("Sign file failed: {}", e)))?;
Ok(signed.raw)
}
pub fn to_yaml(&self, json_str: &str) -> BindingResult<String> {
jacs::convert::jacs_to_yaml(json_str).map_err(|e| conversion_error("to_yaml", e))
}
pub fn from_yaml(&self, yaml_str: &str) -> BindingResult<String> {
jacs::convert::yaml_to_jacs(yaml_str).map_err(|e| conversion_error("from_yaml", e))
}
pub fn to_html(&self, json_str: &str) -> BindingResult<String> {
jacs::convert::jacs_to_html(json_str).map_err(|e| conversion_error("to_html", e))
}
pub fn from_html(&self, html_str: &str) -> BindingResult<String> {
jacs::convert::html_to_jacs(html_str).map_err(|e| conversion_error("from_html", e))
}
pub fn rotate_keys(&self, algorithm: Option<&str>) -> BindingResult<String> {
let result = jacs::simple::advanced::rotate(&self.inner, algorithm).map_err(|e| {
BindingCoreError::new(ErrorKind::Generic, format!("Key rotation failed: {}", e))
})?;
serialize_json(&result, "rotation result")
}
pub fn sign_text_file_json(&self, path: &str, opts_json: &str) -> BindingResult<String> {
let opts = parse_sign_text_options(opts_json)?;
let outcome = jacs::simple::advanced::sign_text_file(&self.inner, path, opts)
.map_err(|e| map_jacs_err(e, "sign_text_file"))?;
serialize_json(&outcome, "sign_text_file outcome")
}
pub fn verify_text_file_json(&self, path: &str, opts_json: &str) -> BindingResult<String> {
let opts = parse_verify_options(opts_json)?;
let strict = opts.strict;
match jacs::simple::advanced::verify_text_file(&self.inner, path, opts) {
Ok(result) => serialize_verify_text_result(&result),
Err(jacs::error::JacsError::MissingSignature(p)) if strict => Err(
BindingCoreError::missing_signature(format!("no JACS signature found in {}", p)),
),
Err(e) => Err(map_jacs_err(e, "verify_text_file")),
}
}
pub fn sign_image_json(
&self,
in_path: &str,
out_path: &str,
opts_json: &str,
) -> BindingResult<String> {
let opts = parse_sign_image_options(opts_json)?;
let outcome = jacs::simple::advanced::sign_image(&self.inner, in_path, out_path, opts)
.map_err(|e| map_jacs_err(e, "sign_image"))?;
serialize_json(&outcome, "sign_image outcome")
}
pub fn verify_image_json(&self, path: &str, opts_json: &str) -> BindingResult<String> {
let opts = parse_verify_image_options(opts_json)?;
let strict = opts.base.strict;
match jacs::simple::advanced::verify_image(&self.inner, path, opts) {
Ok(result) => serialize_json(&result, "verify_image result"),
Err(jacs::error::JacsError::MissingSignature(p)) if strict => Err(
BindingCoreError::missing_signature(format!("no JACS signature found in {}", p)),
),
Err(e) => Err(map_jacs_err(e, "verify_image")),
}
}
pub fn extract_media_signature_json(
&self,
path: &str,
opts_json: &str,
) -> BindingResult<String> {
let parsed = parse_extract_options(opts_json)?;
let opts = jacs::simple::types::ExtractMediaOptions {
scan_robust: parsed.scan_robust,
};
let result = if parsed.raw_payload {
jacs::simple::advanced::extract_media_signature_raw_with_options(path, opts)
} else {
jacs::simple::advanced::extract_media_signature_with_options(path, opts)
};
let payload = result.map_err(|e| map_jacs_err(e, "extract_media_signature"))?;
let envelope = serde_json::json!({
"present": payload.is_some(),
"payload": payload,
});
Ok(envelope.to_string())
}
}
fn map_jacs_err(e: jacs::error::JacsError, op: &str) -> BindingCoreError {
use jacs::error::JacsError;
match e {
JacsError::MissingSignature(p) => BindingCoreError::missing_signature(p),
JacsError::ValidationError(msg) => BindingCoreError::invalid_argument(msg),
JacsError::FileNotFound { path } => {
BindingCoreError::invalid_argument(format!("file not found: {}", path))
}
JacsError::FileReadFailed { path, reason } => {
BindingCoreError::invalid_argument(format!("read {} failed: {}", path, reason))
}
JacsError::FileWriteFailed { path, reason } => BindingCoreError::new(
ErrorKind::Generic,
format!("write {} failed: {}", path, reason),
),
other => BindingCoreError::new(ErrorKind::Generic, format!("{}: {}", op, other)),
}
}
fn opts_is_default(s: &str) -> bool {
let t = s.trim();
t.is_empty() || t == "null" || t == "{}"
}
fn parse_sign_text_options(opts_json: &str) -> BindingResult<jacs::simple::types::SignTextOptions> {
if opts_is_default(opts_json) {
return Ok(jacs::simple::types::SignTextOptions::default());
}
let v: serde_json::Value = serde_json::from_str(opts_json)
.map_err(|e| BindingCoreError::invalid_argument(format!("sign_text_file opts: {}", e)))?;
let mut o = jacs::simple::types::SignTextOptions::default();
if let Some(b) = v.get("backup").and_then(|x| x.as_bool()) {
o.backup = b;
}
if let Some(b) = v.get("allow_duplicate").and_then(|x| x.as_bool()) {
o.allow_duplicate = b;
}
if let Some(b) = v.get("allowDuplicate").and_then(|x| x.as_bool()) {
o.allow_duplicate = b;
}
if let Some(n) = v
.get("unsafeBakMode")
.or_else(|| v.get("unsafe_bak_mode"))
.and_then(|x| x.as_u64())
{
o.unsafe_bak_mode = Some(n as u32);
}
Ok(o)
}
fn parse_verify_options(opts_json: &str) -> BindingResult<jacs::inline::VerifyOptions> {
if opts_is_default(opts_json) {
return Ok(jacs::inline::VerifyOptions::default());
}
let v: serde_json::Value = serde_json::from_str(opts_json)
.map_err(|e| BindingCoreError::invalid_argument(format!("verify opts: {}", e)))?;
let strict = v.get("strict").and_then(|x| x.as_bool()).unwrap_or(false);
let key_dir = v
.get("keyDir")
.or_else(|| v.get("key_dir"))
.and_then(|x| x.as_str())
.map(std::path::PathBuf::from);
Ok(jacs::inline::VerifyOptions { strict, key_dir })
}
fn parse_sign_image_options(
opts_json: &str,
) -> BindingResult<jacs::simple::types::SignImageOptions> {
if opts_is_default(opts_json) {
return Ok(jacs::simple::types::SignImageOptions::default());
}
let v: serde_json::Value = serde_json::from_str(opts_json)
.map_err(|e| BindingCoreError::invalid_argument(format!("sign_image opts: {}", e)))?;
let mut o = jacs::simple::types::SignImageOptions::default();
if let Some(b) = v.get("robust").and_then(|x| x.as_bool()) {
o.robust = b;
}
if let Some(b) = v
.get("refuseOverwrite")
.or_else(|| v.get("refuse_overwrite"))
.and_then(|x| x.as_bool())
{
o.refuse_overwrite = b;
}
if let Some(b) = v.get("backup").and_then(|x| x.as_bool()) {
o.backup = b;
}
if let Some(n) = v
.get("unsafeBakMode")
.or_else(|| v.get("unsafe_bak_mode"))
.and_then(|x| x.as_u64())
{
o.unsafe_bak_mode = Some(n as u32);
}
if let Some(s) = v
.get("formatHint")
.or_else(|| v.get("format_hint"))
.and_then(|x| x.as_str())
{
o.format_hint = Some(s.to_string());
}
Ok(o)
}
fn parse_verify_image_options(
opts_json: &str,
) -> BindingResult<jacs::simple::types::VerifyImageOptions> {
if opts_is_default(opts_json) {
return Ok(jacs::simple::types::VerifyImageOptions::default());
}
let v: serde_json::Value = serde_json::from_str(opts_json)
.map_err(|e| BindingCoreError::invalid_argument(format!("verify_image opts: {}", e)))?;
let strict = v.get("strict").and_then(|x| x.as_bool()).unwrap_or(false);
let key_dir = v
.get("keyDir")
.or_else(|| v.get("key_dir"))
.and_then(|x| x.as_str())
.map(std::path::PathBuf::from);
let scan_robust = v
.get("robust")
.or_else(|| v.get("scan_robust"))
.and_then(|x| x.as_bool())
.unwrap_or(false);
Ok(jacs::simple::types::VerifyImageOptions {
base: jacs::inline::VerifyOptions { strict, key_dir },
scan_robust,
})
}
#[derive(Debug, Clone, Copy, Default)]
struct ParsedExtractOptions {
raw_payload: bool,
scan_robust: bool,
}
fn parse_extract_options(opts_json: &str) -> BindingResult<ParsedExtractOptions> {
if opts_is_default(opts_json) {
return Ok(ParsedExtractOptions::default());
}
let v: serde_json::Value = serde_json::from_str(opts_json).map_err(|e| {
BindingCoreError::invalid_argument(format!("extract_media_signature opts: {}", e))
})?;
let raw_payload = v
.get("rawPayload")
.or_else(|| v.get("raw_payload"))
.and_then(|x| x.as_bool())
.unwrap_or(false);
let scan_robust = v
.get("scanRobust")
.or_else(|| v.get("scan_robust"))
.or_else(|| v.get("robust"))
.and_then(|x| x.as_bool())
.unwrap_or(false);
Ok(ParsedExtractOptions {
raw_payload,
scan_robust,
})
}
fn serialize_verify_text_result(result: &jacs::inline::VerifyTextResult) -> BindingResult<String> {
use jacs::inline::{SignatureStatus, VerifyTextResult};
let v = match result {
VerifyTextResult::MissingSignature => {
serde_json::json!({"status": "missing_signature"})
}
VerifyTextResult::Malformed(detail) => {
serde_json::json!({"status": "malformed", "error": detail})
}
VerifyTextResult::Signed { signatures } => {
let entries: Vec<serde_json::Value> = signatures
.iter()
.map(|e| {
let (status_str, error) = match &e.status {
SignatureStatus::Valid => ("valid", None),
SignatureStatus::InvalidSignature => ("invalid_signature", None),
SignatureStatus::HashMismatch => ("hash_mismatch", None),
SignatureStatus::KeyNotFound => ("key_not_found", None),
SignatureStatus::UnsupportedAlgorithm => ("unsupported_algorithm", None),
SignatureStatus::Malformed(s) => ("malformed", Some(s.clone())),
};
let mut o = serde_json::json!({
"signer_id": e.signer_id,
"algorithm": e.algorithm,
"timestamp": e.timestamp,
"status": status_str,
});
if let Some(err) = error {
o["error"] = serde_json::Value::String(err);
}
o
})
.collect();
serde_json::json!({"status": "signed", "signatures": entries})
}
};
Ok(v.to_string())
}
pub fn sign_message_json(wrapper: &SimpleAgentWrapper, data_json: &str) -> BindingResult<String> {
wrapper.sign_message_json(data_json)
}
pub fn verify_json(wrapper: &SimpleAgentWrapper, signed_document: &str) -> BindingResult<String> {
wrapper.verify_json(signed_document)
}
#[cfg(test)]
mod tests {
use super::*;
fn test_wrapper() -> SimpleAgentWrapper {
let (wrapper, _info) =
SimpleAgentWrapper::ephemeral(Some("ed25519")).expect("ephemeral agent");
wrapper
}
#[test]
fn to_yaml_valid_json_succeeds() {
let wrapper = test_wrapper();
let result = wrapper.to_yaml(r#"{"key": "value"}"#);
assert!(result.is_ok(), "to_yaml should succeed for valid JSON");
let yaml = result.unwrap();
assert!(yaml.contains("key"), "YAML should contain 'key'");
assert!(yaml.contains("value"), "YAML should contain 'value'");
}
#[test]
fn from_yaml_valid_yaml_succeeds() {
let wrapper = test_wrapper();
let result = wrapper.from_yaml("key: value\n");
assert!(result.is_ok(), "from_yaml should succeed for valid YAML");
let json = result.unwrap();
assert!(json.contains("\"key\""), "JSON should contain key");
assert!(json.contains("\"value\""), "JSON should contain value");
}
#[test]
fn to_html_valid_json_succeeds() {
let wrapper = test_wrapper();
let result = wrapper.to_html(r#"{"key": "value"}"#);
assert!(result.is_ok(), "to_html should succeed for valid JSON");
let html = result.unwrap();
assert!(html.contains("<!DOCTYPE html>"), "HTML should have DOCTYPE");
assert!(
html.contains(r#"id="jacs-data">"#),
"HTML should have jacs-data script tag"
);
}
#[test]
fn from_html_valid_html_succeeds() {
let wrapper = test_wrapper();
let json = r#"{"key": "value"}"#;
let html = wrapper.to_html(json).unwrap();
let result = wrapper.from_html(&html);
assert!(result.is_ok(), "from_html should succeed for valid HTML");
assert_eq!(result.unwrap(), json, "Extracted JSON should match input");
}
#[test]
fn yaml_round_trip_preserves_content() {
let wrapper = test_wrapper();
let json = r#"{"hello": "world", "count": 42}"#;
let yaml = wrapper.to_yaml(json).unwrap();
let back = wrapper.from_yaml(&yaml).unwrap();
let original: serde_json::Value = serde_json::from_str(json).unwrap();
let reconstituted: serde_json::Value = serde_json::from_str(&back).unwrap();
assert_eq!(
original, reconstituted,
"YAML round-trip should preserve content"
);
}
#[test]
fn html_round_trip_preserves_content() {
let wrapper = test_wrapper();
let json = r#"{"hello": "world", "count": 42}"#;
let html = wrapper.to_html(json).unwrap();
let back = wrapper.from_html(&html).unwrap();
assert_eq!(back, json, "HTML round-trip should preserve exact JSON");
}
#[test]
fn to_yaml_invalid_json_returns_serialization_failed() {
let wrapper = test_wrapper();
let result = wrapper.to_yaml("{not valid json}");
assert!(result.is_err(), "to_yaml should fail for invalid JSON");
let err = result.unwrap_err();
assert_eq!(
err.kind,
crate::ErrorKind::SerializationFailed,
"Error should be SerializationFailed, got: {:?}",
err.kind
);
}
#[test]
fn from_yaml_invalid_yaml_returns_serialization_failed() {
let wrapper = test_wrapper();
let result = wrapper.from_yaml("{{{{ not yaml ::::");
assert!(result.is_err(), "from_yaml should fail for invalid YAML");
let err = result.unwrap_err();
assert_eq!(
err.kind,
crate::ErrorKind::SerializationFailed,
"Error should be SerializationFailed, got: {:?}",
err.kind
);
}
#[test]
fn from_html_no_script_tag_returns_serialization_failed() {
let wrapper = test_wrapper();
let result = wrapper.from_html("<html><body>No jacs data here</body></html>");
assert!(
result.is_err(),
"from_html should fail without jacs-data tag"
);
let err = result.unwrap_err();
assert_eq!(
err.kind,
crate::ErrorKind::SerializationFailed,
"Error should be SerializationFailed, got: {:?}",
err.kind
);
}
#[test]
fn parse_sign_text_options_honours_unsafe_bak_mode_snake_case() {
let opts =
parse_sign_text_options(r#"{"unsafe_bak_mode": 420}"#).expect("parse should succeed");
assert_eq!(
opts.unsafe_bak_mode,
Some(420),
"snake_case unsafe_bak_mode must round-trip"
);
}
#[test]
fn parse_sign_text_options_honours_unsafe_bak_mode_camel_case() {
let opts =
parse_sign_text_options(r#"{"unsafeBakMode": 420}"#).expect("parse should succeed");
assert_eq!(
opts.unsafe_bak_mode,
Some(420),
"camelCase unsafeBakMode must round-trip"
);
}
#[test]
fn parse_sign_text_options_default_unsafe_bak_mode_is_none() {
let opts = parse_sign_text_options(r#"{"backup": true}"#).expect("parse should succeed");
assert_eq!(
opts.unsafe_bak_mode, None,
"absent unsafe_bak_mode must remain None (uses 0o600 default at write time)"
);
}
#[test]
fn parse_sign_text_options_combines_with_other_fields() {
let opts = parse_sign_text_options(
r#"{"backup": false, "allowDuplicate": true, "unsafeBakMode": 384}"#,
)
.expect("parse should succeed");
assert_eq!(opts.backup, false);
assert_eq!(opts.allow_duplicate, true);
assert_eq!(opts.unsafe_bak_mode, Some(384));
}
#[test]
fn verify_text_file_json_non_existent_path_returns_invalid_argument() {
let wrapper = test_wrapper();
let result =
wrapper.verify_text_file_json("/tmp/jacs-binding-core-r008-does-not-exist.md", "{}");
assert!(result.is_err(), "verify on non-existent path should fail");
let err = result.unwrap_err();
assert_eq!(
err.kind,
crate::ErrorKind::InvalidArgument,
"expected InvalidArgument for non-existent path, got: {:?}",
err.kind
);
}
#[test]
fn verify_image_json_non_existent_path_returns_invalid_argument() {
let wrapper = test_wrapper();
let result =
wrapper.verify_image_json("/tmp/jacs-binding-core-r008-does-not-exist.png", "{}");
assert!(result.is_err(), "verify on non-existent path should fail");
let err = result.unwrap_err();
assert_eq!(
err.kind,
crate::ErrorKind::InvalidArgument,
"expected InvalidArgument for non-existent path, got: {:?}",
err.kind
);
}
#[test]
fn parse_extract_options_default_has_no_robust_scan_or_raw() {
let parsed = parse_extract_options("{}").expect("ok");
assert_eq!(parsed.raw_payload, false);
assert_eq!(parsed.scan_robust, false);
}
#[test]
fn parse_extract_options_honours_scan_robust_camel() {
let parsed = parse_extract_options(r#"{"scanRobust": true}"#).expect("ok");
assert!(parsed.scan_robust);
assert!(!parsed.raw_payload);
}
#[test]
fn parse_extract_options_honours_scan_robust_snake() {
let parsed = parse_extract_options(r#"{"scan_robust": true}"#).expect("ok");
assert!(parsed.scan_robust);
}
#[test]
fn parse_extract_options_honours_short_robust_alias() {
let parsed = parse_extract_options(r#"{"robust": true}"#).expect("ok");
assert!(parsed.scan_robust);
}
#[test]
fn parse_extract_options_combines_raw_payload_and_scan_robust() {
let parsed =
parse_extract_options(r#"{"rawPayload": true, "scanRobust": true}"#).expect("ok");
assert!(parsed.raw_payload);
assert!(parsed.scan_robust);
}
#[test]
#[cfg(unix)]
fn sign_text_file_json_routes_unsafe_bak_mode_camel_to_disk() {
use std::os::unix::fs::PermissionsExt;
let wrapper = test_wrapper();
let dir = tempfile::TempDir::new().expect("tempdir");
let path = dir.path().join("doc.md");
std::fs::write(&path, b"# Hello\n\nbody\n").expect("write fixture");
let outcome_json = wrapper
.sign_text_file_json(
path.to_str().unwrap(),
r#"{"backup": true, "unsafeBakMode": 420}"#,
)
.expect("sign_text_file_json should succeed");
let outcome: serde_json::Value =
serde_json::from_str(&outcome_json).expect("outcome is JSON");
let bak_path = outcome
.get("backup_path")
.and_then(|v| v.as_str())
.expect("backup_path present");
let mode = std::fs::metadata(bak_path)
.expect("bak exists")
.permissions()
.mode()
& 0o777;
assert_eq!(
mode, 0o644,
"JSON envelope unsafeBakMode=420 must reach the on-disk .bak; got {:o}",
mode
);
}
#[test]
#[cfg(unix)]
fn sign_text_file_json_default_is_owner_only() {
use std::os::unix::fs::PermissionsExt;
let wrapper = test_wrapper();
let dir = tempfile::TempDir::new().expect("tempdir");
let path = dir.path().join("doc.md");
std::fs::write(&path, b"# Hello\n\nbody\n").expect("write fixture");
let outcome_json = wrapper
.sign_text_file_json(path.to_str().unwrap(), "{}")
.expect("sign_text_file_json default opts should succeed");
let outcome: serde_json::Value =
serde_json::from_str(&outcome_json).expect("outcome is JSON");
let bak_path = outcome
.get("backup_path")
.and_then(|v| v.as_str())
.expect("backup_path present");
let mode = std::fs::metadata(bak_path)
.expect("bak exists")
.permissions()
.mode()
& 0o777;
assert_eq!(
mode, 0o600,
"default .bak mode through JSON envelope must be 0o600; got {:o}",
mode
);
}
#[test]
fn verify_text_file_json_malformed_block_strict_returns_invalid_argument() {
let wrapper = test_wrapper();
let dir = tempfile::TempDir::new().expect("tempdir");
let path = dir.path().join("malformed.md");
std::fs::write(
&path,
b"# Doc\n\n-----BEGIN JACS SIGNATURE-----\nsigner: x\n",
)
.expect("write fixture");
let result = wrapper.verify_text_file_json(path.to_str().unwrap(), r#"{"strict": true}"#);
assert!(
result.is_err(),
"strict verify on malformed block should fail with Err"
);
let err = result.unwrap_err();
assert_eq!(
err.kind,
crate::ErrorKind::InvalidArgument,
"expected InvalidArgument for malformed-block, got: {:?} (msg: {})",
err.kind,
err.message
);
}
#[test]
fn verify_text_file_json_malformed_block_permissive_returns_status() {
let wrapper = test_wrapper();
let dir = tempfile::TempDir::new().expect("tempdir");
let path = dir.path().join("malformed_permissive.md");
std::fs::write(
&path,
b"# Doc\n\n-----BEGIN JACS SIGNATURE-----\nsigner: x\n",
)
.expect("write fixture");
let result = wrapper
.verify_text_file_json(path.to_str().unwrap(), "{}")
.expect("permissive verify of malformed file must NOT error");
let v: serde_json::Value = serde_json::from_str(&result).expect("result is JSON");
let status = v.get("status").and_then(|s| s.as_str()).unwrap_or("");
assert_eq!(
status, "malformed",
"permissive verify must report status=malformed; got JSON: {}",
v
);
}
#[test]
fn verify_text_file_json_unsigned_permissive_returns_ok_status() {
let wrapper = test_wrapper();
let dir = tempfile::TempDir::new().expect("tempdir");
let path = dir.path().join("unsigned.md");
std::fs::write(&path, b"# Plain\n\nno signatures here\n").expect("write fixture");
let result = wrapper
.verify_text_file_json(path.to_str().unwrap(), "{}")
.expect("permissive verify of unsigned file must NOT error");
let v: serde_json::Value = serde_json::from_str(&result).expect("result is JSON");
let status = v.get("status").and_then(|s| s.as_str()).unwrap_or("");
assert_eq!(
status, "missing_signature",
"permissive verify of unsigned file must report status=missing_signature; got JSON: {}",
v
);
}
}