c2pa/settings/mod.rs
1// Copyright 2024 Adobe. All rights reserved.
2// This file is licensed to you under the Apache License,
3// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
4// or the MIT license (http://opensource.org/licenses/MIT),
5// at your option.
6
7// Unless required by applicable law or agreed to in writing,
8// this software is distributed on an "AS IS" BASIS, WITHOUT
9// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
10// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
11// specific language governing permissions and limitations under
12// each license.
13
14/// Settings for configuring the [`Builder`][crate::Builder].
15pub mod builder;
16/// Settings for configuring the [`Settings::signer`].
17pub mod signer;
18
19#[cfg(feature = "file_io")]
20use std::path::Path;
21use std::{
22 cell::RefCell,
23 io::{BufRead, BufReader, Cursor},
24};
25
26use config::{Config, FileFormat};
27use serde_derive::{Deserialize, Serialize};
28use signer::SignerSettings;
29
30use crate::{
31 crypto::base64, http::restricted::HostPattern, settings::builder::BuilderSettings, Error,
32 Result,
33};
34
35const VERSION: u32 = 1;
36
37thread_local!(
38 static SETTINGS: RefCell<Config> =
39 RefCell::new(Config::try_from(&Settings::default()).unwrap_or_default());
40);
41
42// trait used to validate user input to make sure user supplied configurations are valid
43pub(crate) trait SettingsValidate {
44 // returns error if settings are invalid
45 fn validate(&self) -> Result<()> {
46 Ok(())
47 }
48}
49
50/// Settings to configure the trust list.
51#[cfg_attr(
52 feature = "json_schema",
53 derive(schemars::JsonSchema),
54 schemars(default)
55)]
56#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
57pub struct Trust {
58 /// Whether to verify certificates against the trust lists specified in [`Trust`]. This
59 /// option is ONLY applicable to CAWG.
60 ///
61 /// The default value is true.
62 ///
63 /// <div class="warning">
64 /// Verifying trust is REQUIRED by the CAWG spec. This option should only be used for development or testing.
65 /// </div>
66 pub(crate) verify_trust_list: bool,
67 /// List of additional user-provided trust anchor root certificates as a PEM bundle.
68 pub user_anchors: Option<String>,
69 /// List of default trust anchor root certificates as a PEM bundle.
70 ///
71 /// Normally this option contains the official C2PA-recognized trust anchors found here:
72 /// <https://github.com/c2pa-org/conformance-public/tree/main/trust-list>
73 pub trust_anchors: Option<String>,
74 /// List of allowed extended key usage (EKU) object identifiers (OID) that
75 /// certificates must have.
76 pub trust_config: Option<String>,
77 /// List of explicitly allowed certificates as a PEM bundle.
78 pub allowed_list: Option<String>,
79}
80
81impl Trust {
82 // load PEMs
83 fn load_trust_from_data(&self, trust_data: &[u8]) -> Result<Vec<Vec<u8>>> {
84 let mut certs = Vec::new();
85
86 // allow for JSON-encoded PEMs with \n
87 let trust_data = String::from_utf8_lossy(trust_data)
88 .replace("\\n", "\n")
89 .into_bytes();
90 for pem_result in x509_parser::pem::Pem::iter_from_buffer(&trust_data) {
91 let pem = pem_result.map_err(|_e| Error::CoseInvalidCert)?;
92 certs.push(pem.contents);
93 }
94 Ok(certs)
95 }
96
97 // sanity check to see if can parse trust settings
98 fn test_load_trust(&self, allowed_list: &[u8]) -> Result<()> {
99 // check pems
100 if let Ok(cert_list) = self.load_trust_from_data(allowed_list) {
101 if !cert_list.is_empty() {
102 return Ok(());
103 }
104 }
105
106 // try to load the of base64 encoded encoding of the sha256 hash of the certificate DER encoding
107 let reader = Cursor::new(allowed_list);
108 let buf_reader = BufReader::new(reader);
109 let mut found_der_hash = false;
110
111 let mut inside_cert_block = false;
112 for l in buf_reader.lines().map_while(|v| v.ok()) {
113 if l.contains("-----BEGIN") {
114 inside_cert_block = true;
115 }
116 if l.contains("-----END") {
117 inside_cert_block = false;
118 }
119
120 // sanity check that that is is base64 encoded and outside of certificate block
121 if !inside_cert_block && base64::decode(&l).is_ok() && !l.is_empty() {
122 found_der_hash = true;
123 }
124 }
125
126 if found_der_hash {
127 Ok(())
128 } else {
129 Err(Error::CoseInvalidCert)
130 }
131 }
132}
133
134#[allow(clippy::derivable_impls)]
135impl Default for Trust {
136 fn default() -> Self {
137 // load test config store for unit tests
138 #[cfg(test)]
139 {
140 let mut trust = Self {
141 verify_trust_list: true,
142 user_anchors: None,
143 trust_anchors: None,
144 trust_config: None,
145 allowed_list: None,
146 };
147
148 trust.trust_config = Some(
149 String::from_utf8_lossy(include_bytes!(
150 "../../tests/fixtures/certs/trust/store.cfg"
151 ))
152 .into_owned(),
153 );
154 trust.user_anchors = Some(
155 String::from_utf8_lossy(include_bytes!(
156 "../../tests/fixtures/certs/trust/test_cert_root_bundle.pem"
157 ))
158 .into_owned(),
159 );
160
161 trust
162 }
163 #[cfg(not(test))]
164 {
165 Self {
166 verify_trust_list: true,
167 user_anchors: None,
168 trust_anchors: None,
169 trust_config: None,
170 allowed_list: None,
171 }
172 }
173 }
174}
175
176impl SettingsValidate for Trust {
177 fn validate(&self) -> Result<()> {
178 if let Some(ta) = &self.trust_anchors {
179 self.test_load_trust(ta.as_bytes())?;
180 }
181
182 if let Some(pa) = &self.user_anchors {
183 self.test_load_trust(pa.as_bytes())?;
184 }
185
186 if let Some(al) = &self.allowed_list {
187 self.test_load_trust(al.as_bytes())?;
188 }
189
190 Ok(())
191 }
192}
193
194/// Settings to configure core features.
195#[cfg_attr(
196 feature = "json_schema",
197 derive(schemars::JsonSchema),
198 schemars(default)
199)]
200#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
201pub struct Core {
202 /// Size of the [`BmffHash`] merkle tree chunks in kilobytes.
203 ///
204 /// This option is associated with the [`MerkleMap::fixed_block_size`] field.
205 ///
206 /// See more information in the spec here:
207 /// [bmff_based_hash - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_bmff_based_hash)
208 ///
209 /// [`MerkleMap::fixed_block_size`]: crate::assertions::MerkleMap::fixed_block_size
210 /// [`BmffHash`]: crate::assertions::BmffHash
211 pub merkle_tree_chunk_size_in_kb: Option<usize>,
212 /// Maximum number of proofs when validating or writing a [`BmffHash`] merkle tree.
213 ///
214 /// This option defaults to 5.
215 ///
216 /// See more information in the spec here:
217 /// [bmff_based_hash - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_bmff_based_hash)
218 ///
219 /// [`BmffHash`]: crate::assertions::BmffHash
220 pub merkle_tree_max_proofs: usize,
221 /// Maximum amount of data in megabytes that will be loaded into memory before
222 /// being stored in temporary files on the disk.
223 ///
224 /// This option defaults to 512MB and can result in noticeable performance improvements.
225 pub backing_store_memory_threshold_in_mb: usize,
226 /// Whether to decode CAWG [`IdentityAssertion`]s during reading in the [`Reader`].
227 ///
228 /// This option defaults to true.
229 ///
230 /// [`IdentityAssertion`]: crate::identity::IdentityAssertion
231 /// [`Reader`]: crate::Reader
232 pub decode_identity_assertions: bool,
233 /// <div class="warning">
234 /// The CAWG identity assertion does not currently respect this setting.
235 /// See [Issue #1645](https://github.com/contentauth/c2pa-rs/issues/1645).
236 /// </div>
237 ///
238 /// List of host patterns that are allowed for network requests.
239 ///
240 /// Each pattern may include:
241 /// - A scheme (e.g. `https://` or `http://`)
242 /// - A hostname or IP address (e.g. `contentauthenticity.org` or `192.0.2.1`)
243 /// - The hostname may contain a single leading wildcard (e.g. `*.contentauthenticity.org`)
244 /// - An optional port (e.g. `contentauthenticity.org:443` or `192.0.2.1:8080`)
245 ///
246 /// Matching is case-insensitive. A wildcard pattern such as `*.contentauthenticity.org` matches
247 /// `sub.contentauthenticity.org`, but does not match `contentauthenticity.org` or `fakecontentauthenticity.org`.
248 /// If a scheme is present in the pattern, only URIs using the same scheme are considered a match. If the scheme
249 /// is omitted, any scheme is allowed as long as the host matches.
250 ///
251 /// The behavior is as follows:
252 /// - `None` (default) no filtering enabled.
253 /// - `Some(vec)` where `vec` is empty, all traffic is blocked.
254 /// - `Some(vec)` with at least one pattern, filtering enabled for only those patterns.
255 ///
256 /// # Examples
257 ///
258 /// Pattern: `*.contentauthenticity.org`
259 /// - Does match:
260 /// - `https://sub.contentauthenticity.org`
261 /// - `http://api.contentauthenticity.org`
262 /// - Does **not** match:
263 /// - `https://contentauthenticity.org` (no subdomain)
264 /// - `https://sub.fakecontentauthenticity.org` (different host)
265 ///
266 /// Pattern: `http://192.0.2.1:8080`
267 /// - Does match:
268 /// - `http://192.0.2.1:8080`
269 /// - Does **not** match:
270 /// - `https://192.0.2.1:8080` (scheme mismatch)
271 /// - `http://192.0.2.1` (port omitted)
272 /// - `http://192.0.2.2:8080` (different IP address)
273 ///
274 /// These settings are applied by the SDK's HTTP resolvers to restrict network requests.
275 /// When network requests occur depends on the operations being performed (reading manifests,
276 /// validating credentials, timestamping, etc.).
277 pub allowed_network_hosts: Option<Vec<HostPattern>>,
278}
279
280impl Default for Core {
281 fn default() -> Self {
282 Self {
283 merkle_tree_chunk_size_in_kb: None,
284 merkle_tree_max_proofs: 5,
285 backing_store_memory_threshold_in_mb: 512,
286 decode_identity_assertions: true,
287 allowed_network_hosts: None,
288 }
289 }
290}
291
292impl SettingsValidate for Core {
293 fn validate(&self) -> Result<()> {
294 Ok(())
295 }
296}
297
298/// Settings to configure the verification process.
299#[cfg_attr(
300 feature = "json_schema",
301 derive(schemars::JsonSchema),
302 schemars(default)
303)]
304#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
305pub struct Verify {
306 /// Whether to verify the manifest after reading in the [`Reader`].
307 ///
308 /// The default value is true.
309 ///
310 /// <div class="warning">
311 /// Disabling validation can improve reading performance, BUT it carries the risk of reading an invalid
312 /// manifest.
313 /// </div>
314 ///
315 /// [`Reader`]: crate::Reader
316 pub verify_after_reading: bool,
317 /// Whether to verify the manifest after signing in the [`Builder`].
318 ///
319 /// The default value is false.
320 /// There is a known bug related to this setting: [#1875](https://github.com/contentauth/c2pa-rs/issues/1875).
321 /// When the bug is fixed, the default value should be true.
322 ///
323 /// <div class="warning">
324 /// Disabling validation can improve signing performance, BUT it carries the risk of signing an invalid
325 /// manifest.
326 /// </div>
327 ///
328 /// [`Builder`]: crate::Builder
329 pub verify_after_sign: bool,
330 /// Whether to verify certificates against the trust lists specified in [`Trust`]. To configure
331 /// timestamp certificate verification, see [`Verify::verify_timestamp_trust`].
332 ///
333 /// The default value is true.
334 ///
335 /// <div class="warning">
336 /// Verifying trust is REQUIRED by the C2PA spec. This option should only be used for development or testing.
337 /// </div>
338 pub(crate) verify_trust: bool,
339 /// Whether to verify the timestamp certificates against the trust lists specified in [`Trust`].
340 ///
341 /// The default value is true.
342 ///
343 /// <div class="warning">
344 /// Verifying timestamp trust is REQUIRED by the C2PA spec. This option should only be used for development or testing.
345 /// </div>
346 pub(crate) verify_timestamp_trust: bool,
347 /// Whether to fetch the certificates OCSP status during validation.
348 ///
349 /// Revocation status is checked in the following order:
350 /// 1. The OCSP staple stored in the COSE claim of the manifest
351 /// 2. Otherwise if `ocsp_fetch` is enabled, it fetches a new OCSP status
352 /// 3. Otherwise if `ocsp_fetch` is disabled, it checks `CertificateStatus` assertions
353 ///
354 /// The default value is false.
355 pub ocsp_fetch: bool,
356 /// Whether to fetch remote manifests in the following scenarios:
357 /// - Constructing a [`Reader`]
358 /// - Adding an [`Ingredient`] to the [`Builder`]
359 ///
360 /// The default value is true.
361 ///
362 /// <div class="warning">
363 /// This setting is only applicable if the crate is compiled with the `fetch_remote_manifests` feature.
364 /// </div>
365 ///
366 /// [`Reader`]: crate::Reader
367 /// [`Ingredient`]: crate::Ingredient
368 /// [`Builder`]: crate::Builder
369 pub remote_manifest_fetch: bool,
370 /// Whether to skip ingredient conflict resolution when multiple ingredients have the same
371 /// manifest identifier. This settings is only applicable for C2PA v2 validation.
372 ///
373 /// The default value is false.
374 ///
375 /// See more information in the spec here:
376 /// [versioning_manifests_due_to_conflicts - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_versioning_manifests_due_to_conflicts)
377 pub(crate) skip_ingredient_conflict_resolution: bool,
378 /// Whether to do strictly C2PA v1 validation or otherwise the latest validation.
379 ///
380 /// The default value is false.
381 pub strict_v1_validation: bool,
382}
383
384impl Default for Verify {
385 fn default() -> Self {
386 Self {
387 verify_after_reading: true,
388 verify_after_sign: false, // TODO: Update docs when #1875 is fixed.
389 verify_trust: true,
390 verify_timestamp_trust: !cfg!(test), // verify timestamp trust unless in test mode
391 ocsp_fetch: false,
392 remote_manifest_fetch: true,
393 skip_ingredient_conflict_resolution: false,
394 strict_v1_validation: false,
395 }
396 }
397}
398
399impl SettingsValidate for Verify {}
400
401/// Settings for configuring all aspects of c2pa-rs.
402///
403/// [Settings::default] will be set thread-locally by default. Any settings set via
404/// [Settings::from_toml] or [Settings::from_file] will also be thread-local.
405#[cfg_attr(
406 feature = "json_schema",
407 derive(schemars::JsonSchema),
408 schemars(default)
409)]
410#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
411pub struct Settings {
412 /// Version of the configuration.
413 pub version: u32,
414 // TODO (https://github.com/contentauth/c2pa-rs/issues/1314):
415 // Rename to c2pa_trust? Discuss possibly breaking change.
416 /// Settings for configuring the C2PA trust lists.
417 pub trust: Trust,
418 /// Settings for configuring the CAWG trust lists.
419 pub cawg_trust: Trust,
420 /// Settings for configuring core features.
421 pub core: Core,
422 /// Settings for configuring verification.
423 pub verify: Verify,
424 /// Settings for configuring the [`Builder`].
425 ///
426 /// [`Builder`]: crate::Builder
427 pub builder: BuilderSettings,
428 /// Settings for configuring the base C2PA signer, accessible via [`Settings::signer`].
429 #[serde(skip_serializing_if = "Option::is_none")]
430 pub signer: Option<SignerSettings>,
431 /// Settings for configuring the CAWG x509 signer, accessible via [`Settings::signer`].
432 #[serde(skip_serializing_if = "Option::is_none")]
433 pub cawg_x509_signer: Option<SignerSettings>,
434}
435
436impl Settings {
437 #[cfg(feature = "file_io")]
438 /// Load thread-local [Settings] from a file.
439 /// to be deprecated - use [Settings::with_file] instead
440 #[doc(hidden)]
441 pub fn from_file<P: AsRef<Path>>(settings_path: P) -> Result<Self> {
442 let ext = settings_path
443 .as_ref()
444 .extension()
445 .ok_or(Error::UnsupportedType)?
446 .to_string_lossy();
447
448 let setting_buf = std::fs::read(&settings_path).map_err(Error::IoError)?;
449 Settings::from_string(&String::from_utf8_lossy(&setting_buf), &ext)
450 }
451
452 /// Load thread-local [Settings] from string representation of the configuration.
453 /// Format of configuration must be supplied (json or toml).
454 /// to be deprecated - use [Settings::with_json] or [Settings::with_toml] instead
455 #[doc(hidden)]
456 pub fn from_string(settings_str: &str, format: &str) -> Result<Self> {
457 let f = match format.to_lowercase().as_str() {
458 "json" => FileFormat::Json,
459 "toml" => FileFormat::Toml,
460 _ => return Err(Error::UnsupportedType),
461 };
462
463 let new_config = Config::builder()
464 .add_source(config::File::from_str(settings_str, f))
465 .build()
466 .map_err(|_e| Error::BadParam("could not parse configuration file".into()))?;
467
468 let update_config = SETTINGS.with_borrow(|current_settings| {
469 Config::builder()
470 .add_source(current_settings.clone())
471 .add_source(new_config)
472 .build() // merge overrides, allows for partial changes
473 });
474
475 match update_config {
476 Ok(update_config) => {
477 // sanity check the values before committing
478 let settings = update_config
479 .clone()
480 .try_deserialize::<Settings>()
481 .map_err(|e| Error::BadParam(e.to_string()))?;
482
483 settings.validate()?;
484
485 SETTINGS.set(update_config.clone());
486
487 Ok(settings)
488 }
489 Err(_) => Err(Error::OtherError("could not update configuration".into())),
490 }
491 }
492
493 /// Set the thread-local [Settings] from a toml file.
494 /// to be deprecated use [Settings::with_toml] instead
495 pub fn from_toml(toml: &str) -> Result<()> {
496 Settings::from_string(toml, "toml").map(|_| ())
497 }
498
499 /// Update this `Settings` instance from a string representation.
500 /// This overlays the provided configuration on top of the current settings
501 /// without affecting the thread-local settings.
502 ///
503 /// # Arguments
504 /// * `settings_str` - The configuration string
505 /// * `format` - The format of the configuration ("json" or "toml")
506 ///
507 /// # Example
508 /// ```
509 /// use c2pa::settings::Settings;
510 ///
511 /// let mut settings = Settings::default();
512 ///
513 /// // Update with TOML
514 /// settings
515 /// .update_from_str(
516 /// r#"
517 /// [verify]
518 /// verify_after_sign = false
519 /// "#,
520 /// "toml",
521 /// )
522 /// .unwrap();
523 ///
524 /// assert!(!settings.verify.verify_after_sign);
525 ///
526 /// // Update with JSON (can set values to null)
527 /// settings
528 /// .update_from_str(
529 /// r#"
530 /// {
531 /// "verify": {
532 /// "verify_after_sign": true
533 /// }
534 /// }
535 /// "#,
536 /// "json",
537 /// )
538 /// .unwrap();
539 ///
540 /// assert!(settings.verify.verify_after_sign);
541 /// ```
542 pub fn update_from_str(&mut self, settings_str: &str, format: &str) -> Result<()> {
543 let file_format = match format.to_lowercase().as_str() {
544 "json" => FileFormat::Json,
545 "toml" => FileFormat::Toml,
546 _ => return Err(Error::UnsupportedType),
547 };
548
549 // Convert current settings to Config
550 let current_config = Config::try_from(&*self)
551 .map_err(|e| Error::BadParam(format!("could not convert settings: {e}")))?;
552
553 // Build new config with the source
554 let merged_config = Config::builder()
555 .add_source(current_config)
556 .add_source(config::File::from_str(settings_str, file_format))
557 .build()
558 .map_err(|e| Error::BadParam(format!("could not merge configuration: {e}")))?;
559
560 // Deserialize and validate
561 let updated_settings = merged_config
562 .try_deserialize::<Settings>()
563 .map_err(|e| Error::BadParam(e.to_string()))?;
564
565 updated_settings.validate()?;
566
567 *self = updated_settings;
568 Ok(())
569 }
570
571 /// Set a [Settings] value by path reference. The path is nested names of of the Settings objects
572 /// separated by "." notation.
573 ///
574 /// For example "core.hash_alg" would set settings.core.hash_alg value. The nesting can be arbitrarily
575 /// deep based on the [Settings] definition.
576 #[allow(unused)]
577 pub(crate) fn set_thread_local_value<T: Into<config::Value>>(
578 value_path: &str,
579 value: T,
580 ) -> Result<()> {
581 let c = SETTINGS.take();
582
583 let update_config = Config::builder()
584 .add_source(c.clone())
585 .set_override(value_path, value);
586
587 if let Ok(updated) = update_config {
588 let update_config = updated
589 .build()
590 .map_err(|_e| Error::OtherError("could not update configuration".into()))?;
591
592 let settings = update_config
593 .clone()
594 .try_deserialize::<Settings>()
595 .map_err(|e| Error::BadParam(e.to_string()))?;
596 settings.validate()?;
597
598 SETTINGS.set(update_config);
599
600 Ok(())
601 } else {
602 SETTINGS.set(c);
603 Err(Error::OtherError("could not save settings".into()))
604 }
605 }
606
607 /// Get a [Settings] value by path reference from the thread-local settings.
608 /// The path is nested names of of the [Settings] objects
609 /// separated by "." notation.
610 ///
611 /// For example "core.hash_alg" would get the settings.core.hash_alg value. The nesting can be arbitrarily
612 /// deep based on the [Settings] definition.
613 #[allow(unused)]
614 fn get_thread_local_value<'de, T: serde::de::Deserialize<'de>>(value_path: &str) -> Result<T> {
615 SETTINGS.with_borrow(|current_settings| {
616 let update_config = Config::builder()
617 .add_source(current_settings.clone())
618 .build()
619 .map_err(|_e| Error::OtherError("could not update configuration".into()))?;
620
621 update_config
622 .get::<T>(value_path)
623 .map_err(|_| Error::BadParam("could not get settings value".into()))
624 })
625 }
626
627 /// Set the thread-local [Settings] back to the default values.
628 /// to be deprecated
629 #[allow(unused)]
630 pub(crate) fn reset() -> Result<()> {
631 if let Ok(default_settings) = Config::try_from(&Settings::default()) {
632 SETTINGS.set(default_settings);
633 Ok(())
634 } else {
635 Err(Error::OtherError("could not reset settings".into()))
636 }
637 }
638
639 /// Creates a new Settings instance with default values.
640 ///
641 /// This is the starting point for the builder pattern. Use with `.with_json()`,
642 /// `.with_toml()`, or `.with_value()` to configure settings without touching
643 /// thread-local state.
644 ///
645 /// # Examples
646 ///
647 /// ```
648 /// # use c2pa::settings::Settings;
649 /// # use c2pa::Context;
650 /// # fn main() -> c2pa::Result<()> {
651 /// let settings = Settings::new().with_json(r#"{"verify": {"verify_trust": true}}"#)?;
652 /// let context = Context::new().with_settings(settings)?;
653 /// # Ok(())
654 /// # }
655 /// ```
656 pub fn new() -> Self {
657 Self::default()
658 }
659
660 /// Load settings from JSON string using the builder pattern.
661 ///
662 /// This does NOT update thread-local settings. It overlays the JSON configuration
663 /// on top of the current Settings instance.
664 ///
665 /// # Arguments
666 ///
667 /// * `json` - JSON string containing settings configuration
668 ///
669 /// # Examples
670 ///
671 /// ```
672 /// # use c2pa::settings::Settings;
673 /// # fn main() -> c2pa::Result<()> {
674 /// let settings = Settings::new().with_json(r#"{"verify": {"verify_trust": true}}"#)?;
675 /// # Ok(())
676 /// # }
677 /// ```
678 pub fn with_json(self, json: &str) -> Result<Self> {
679 self.with_string(json, "json")
680 }
681
682 /// Load settings from TOML string using the builder pattern.
683 ///
684 /// This does NOT update thread-local settings. It overlays the TOML configuration
685 /// on top of the current Settings instance. For the legacy behavior that
686 /// updates thread-locals, use `Settings::from_toml()`.
687 ///
688 /// # Arguments
689 ///
690 /// * `toml` - TOML string containing settings configuration
691 ///
692 /// # Examples
693 ///
694 /// ```
695 /// # use c2pa::settings::Settings;
696 /// # fn main() -> c2pa::Result<()> {
697 /// let settings = Settings::new().with_toml(
698 /// r#"
699 /// [verify]
700 /// verify_trust = true
701 /// "#,
702 /// )?;
703 /// # Ok(())
704 /// # }
705 /// ```
706 pub fn with_toml(self, toml: &str) -> Result<Self> {
707 self.with_string(toml, "toml")
708 }
709
710 /// Load settings from a file using the builder pattern.
711 ///
712 /// The file format (JSON or TOML) is inferred from the file extension.
713 /// This does NOT update thread-local settings.
714 ///
715 /// # Arguments
716 ///
717 /// * `path` - Path to the settings file
718 ///
719 /// # Examples
720 ///
721 /// ```no_run
722 /// # use c2pa::settings::Settings;
723 /// # fn main() -> c2pa::Result<()> {
724 /// let settings = Settings::new().with_file("config.toml")?;
725 /// # Ok(())
726 /// # }
727 /// ```
728 #[cfg(feature = "file_io")]
729 pub fn with_file<P: AsRef<Path>>(self, path: P) -> Result<Self> {
730 let path = path.as_ref();
731 let ext = path
732 .extension()
733 .ok_or(Error::BadParam(
734 "settings file must have json or toml extension".into(),
735 ))?
736 .to_str()
737 .ok_or(Error::BadParam("invalid settings file name".into()))?;
738 let setting_buf = std::fs::read(path).map_err(Error::IoError)?;
739 self.with_string(&String::from_utf8_lossy(&setting_buf), ext)
740 }
741
742 /// Load settings from string representation (builder pattern helper).
743 ///
744 /// This overlays the parsed configuration on top of the current Settings
745 /// instance without touching thread-local state.
746 fn with_string(self, settings_str: &str, format: &str) -> Result<Self> {
747 let f = match format.to_lowercase().as_str() {
748 "json" => FileFormat::Json,
749 "toml" => FileFormat::Toml,
750 _ => return Err(Error::UnsupportedType),
751 };
752
753 // Convert current settings to Config
754 let current_config = Config::try_from(&self).map_err(|e| Error::OtherError(Box::new(e)))?;
755
756 // Parse new config and overlay it on current
757 let updated_config = Config::builder()
758 .add_source(current_config)
759 .add_source(config::File::from_str(settings_str, f))
760 .build()
761 .map_err(|_e| Error::BadParam("could not parse configuration".into()))?;
762
763 // Deserialize back to Settings
764 let settings = updated_config
765 .try_deserialize::<Settings>()
766 .map_err(|e| Error::BadParam(e.to_string()))?;
767
768 // Validate
769 settings.validate()?;
770
771 Ok(settings)
772 }
773
774 /// Serializes the thread-local [Settings] into a toml string.
775 #[doc(hidden)]
776 pub fn to_toml() -> Result<String> {
777 let settings = get_thread_local_settings();
778 Ok(toml::to_string(&settings)?)
779 }
780
781 /// Serializes the thread-local [Settings] into a pretty (formatted) toml string.
782 #[doc(hidden)]
783 pub fn to_pretty_toml() -> Result<String> {
784 let settings = get_thread_local_settings();
785 Ok(toml::to_string_pretty(&settings)?)
786 }
787
788 /// Returns the constructed signer from the `signer` field.
789 ///
790 /// If the signer settings aren't specified, this function will return [Error::MissingSignerSettings].
791 #[inline]
792 pub fn signer() -> Result<crate::BoxedSigner> {
793 SignerSettings::signer()
794 }
795
796 /// Sets a value at the specified path in this Settings instance using the builder pattern.
797 ///
798 /// The path uses dot notation to navigate nested structures.
799 /// For example: "verify.verify_trust", "core.hash_alg", "builder.thumbnail.enabled"
800 ///
801 /// # Arguments
802 ///
803 /// * `path` - A dot-separated path to the setting (e.g., "verify.verify_trust")
804 /// * `value` - Any value that can be converted into a config::Value
805 ///
806 /// # Returns
807 ///
808 /// The updated Settings instance (for chaining)
809 ///
810 /// # Errors
811 ///
812 /// Returns an error if:
813 /// - The path is invalid
814 /// - The value type doesn't match the expected type
815 /// - Validation fails after the change
816 ///
817 /// # Examples
818 ///
819 /// ```
820 /// # use c2pa::settings::Settings;
821 /// # fn main() -> c2pa::Result<()> {
822 /// let settings = Settings::default()
823 /// .with_value("verify.verify_trust", true)?
824 /// .with_value("core.merkle_tree_max_proofs", 10)?
825 /// .with_value("core.backing_store_memory_threshold_in_mb", 256)?;
826 /// # Ok(())
827 /// # }
828 /// ```
829 pub fn with_value<T: Into<config::Value>>(self, path: &str, value: T) -> Result<Self> {
830 // Convert self to Config
831 let config = Config::try_from(&self).map_err(|e| Error::OtherError(Box::new(e)))?;
832
833 // Apply the override
834 let updated_config = Config::builder()
835 .add_source(config)
836 .set_override(path, value)
837 .map_err(|e| Error::BadParam(format!("Invalid path '{path}': {e}")))?
838 .build()
839 .map_err(|e| Error::OtherError(Box::new(e)))?;
840
841 // Deserialize back to Settings
842 let updated_settings = updated_config
843 .try_deserialize::<Settings>()
844 .map_err(|e| Error::BadParam(format!("Invalid value for '{path}': {e}")))?;
845
846 // Validate the updated settings
847 updated_settings.validate()?;
848
849 Ok(updated_settings)
850 }
851
852 /// Sets a value at the specified path, modifying this Settings instance in place.
853 ///
854 /// This is a mutable alternative to [`with_value`](Settings::with_value).
855 ///
856 /// # Arguments
857 ///
858 /// * `path` - A dot-separated path to the setting
859 /// * `value` - Any value that can be converted into a config::Value
860 ///
861 /// # Errors
862 ///
863 /// Returns an error if:
864 /// - The path is invalid
865 /// - The value type doesn't match the expected type
866 /// - Validation fails after the change
867 ///
868 /// # Examples
869 ///
870 /// ```
871 /// # use c2pa::settings::Settings;
872 /// # fn main() -> c2pa::Result<()> {
873 /// let mut settings = Settings::default();
874 /// settings.set_value("verify.verify_trust", false)?;
875 /// settings.set_value("core.merkle_tree_max_proofs", 10)?;
876 /// # Ok(())
877 /// # }
878 /// ```
879 pub fn set_value<T: Into<config::Value>>(&mut self, path: &str, value: T) -> Result<()> {
880 *self = std::mem::take(self).with_value(path, value)?;
881 Ok(())
882 }
883
884 /// Gets a value at the specified path from this Settings instance.
885 ///
886 /// The path uses dot notation to navigate nested structures.
887 /// The return type is inferred from context or can be specified explicitly.
888 ///
889 /// # Arguments
890 ///
891 /// * `path` - A dot-separated path to the setting
892 ///
893 /// # Type Parameters
894 ///
895 /// * `T` - The expected type of the value (must implement Deserialize)
896 ///
897 /// # Returns
898 ///
899 /// The value at the specified path
900 ///
901 /// # Errors
902 ///
903 /// Returns an error if:
904 /// - The path doesn't exist
905 /// - The value can't be deserialized to type T
906 ///
907 /// # Examples
908 ///
909 /// ```
910 /// # use c2pa::settings::Settings;
911 /// # fn main() -> c2pa::Result<()> {
912 /// let settings = Settings::default();
913 ///
914 /// // Type can be inferred
915 /// let verify_trust: bool = settings.get_value("verify.verify_trust")?;
916 ///
917 /// // Or specified explicitly
918 /// let max_proofs = settings.get_value::<usize>("core.merkle_tree_max_proofs")?;
919 /// # Ok(())
920 /// # }
921 /// ```
922 pub fn get_value<'de, T: serde::de::Deserialize<'de>>(&self, path: &str) -> Result<T> {
923 let config = Config::try_from(self).map_err(|e| Error::OtherError(Box::new(e)))?;
924
925 config
926 .get::<T>(path)
927 .map_err(|e| Error::BadParam(format!("Failed to get value at '{path}': {e}")))
928 }
929}
930
931impl Default for Settings {
932 fn default() -> Self {
933 Settings {
934 version: VERSION,
935 trust: Default::default(),
936 cawg_trust: Default::default(),
937 core: Default::default(),
938 verify: Default::default(),
939 builder: Default::default(),
940 signer: None,
941 cawg_x509_signer: None,
942 }
943 }
944}
945
946impl SettingsValidate for Settings {
947 fn validate(&self) -> Result<()> {
948 if self.version > VERSION {
949 return Err(Error::VersionCompatibility(
950 "settings version too new".into(),
951 ));
952 }
953 if let Some(signer) = &self.signer {
954 signer.validate()?;
955 }
956 if let Some(cawg_x509_signer) = &self.cawg_x509_signer {
957 cawg_x509_signer.validate()?;
958 }
959 self.trust.validate()?;
960 self.cawg_trust.validate()?;
961 self.core.validate()?;
962 self.builder.validate()
963 }
964}
965
966/// Get a snapshot of the thread-local Settings, always returns a valid Settings object.
967/// If the thread-local settings cannot be deserialized, returns default Settings.
968#[allow(unused)]
969pub(crate) fn get_thread_local_settings() -> Settings {
970 SETTINGS.with_borrow(|config| {
971 config
972 .clone()
973 .try_deserialize::<Settings>()
974 .unwrap_or_default()
975 })
976}
977
978// Save the current configuration to a json file.
979
980/// See [Settings::set_thread_local_value] for more information.
981#[cfg(test)]
982pub(crate) fn set_settings_value<T: Into<config::Value>>(value_path: &str, value: T) -> Result<()> {
983 Settings::set_thread_local_value(value_path, value)
984}
985
986/// See [Settings::get_thread_local_value] for more information.
987#[cfg(test)]
988fn get_settings_value<'de, T: serde::de::Deserialize<'de>>(value_path: &str) -> Result<T> {
989 Settings::get_thread_local_value(value_path)
990}
991
992/// Reset all settings back to default values.
993#[cfg(test)]
994// #[deprecated = "use `Settings::reset` instead"]
995pub fn reset_default_settings() -> Result<()> {
996 Settings::reset()
997}
998
999#[cfg(test)]
1000pub mod tests {
1001 #![allow(clippy::panic)]
1002 #![allow(clippy::unwrap_used)]
1003
1004 use super::*;
1005 #[cfg(feature = "file_io")]
1006 use crate::utils::io_utils::tempdirectory;
1007 use crate::{utils::test::test_settings, SigningAlg};
1008
1009 #[cfg(feature = "file_io")]
1010 fn save_settings_as_json<P: AsRef<Path>>(settings_path: P) -> Result<()> {
1011 let settings = get_thread_local_settings();
1012
1013 let settings_json = serde_json::to_string_pretty(&settings).map_err(Error::JsonError)?;
1014
1015 std::fs::write(settings_path, settings_json.as_bytes()).map_err(Error::IoError)
1016 }
1017
1018 #[test]
1019 fn test_get_defaults() {
1020 let settings = get_thread_local_settings();
1021
1022 assert_eq!(settings.core, Core::default());
1023 assert_eq!(settings.trust, Trust::default());
1024 assert_eq!(settings.cawg_trust, Trust::default());
1025 assert_eq!(settings.verify, Verify::default());
1026 assert_eq!(settings.builder, BuilderSettings::default());
1027
1028 reset_default_settings().unwrap();
1029 }
1030
1031 #[test]
1032 fn test_get_val_by_direct_path() {
1033 // you can do this for all values but if these sanity checks pass they all should if the path is correct
1034 assert_eq!(
1035 get_settings_value::<bool>("builder.thumbnail.enabled").unwrap(),
1036 BuilderSettings::default().thumbnail.enabled
1037 );
1038 assert_eq!(
1039 get_settings_value::<Option<String>>("trust.user_anchors").unwrap(),
1040 Trust::default().user_anchors
1041 );
1042
1043 // test getting full objects
1044 assert_eq!(get_settings_value::<Core>("core").unwrap(), Core::default());
1045 assert_eq!(
1046 get_settings_value::<Verify>("verify").unwrap(),
1047 Verify::default()
1048 );
1049 assert_eq!(
1050 get_settings_value::<BuilderSettings>("builder").unwrap(),
1051 BuilderSettings::default()
1052 );
1053 assert_eq!(
1054 get_settings_value::<Trust>("trust").unwrap(),
1055 Trust::default()
1056 );
1057
1058 // test implicit deserialization
1059 let remote_manifest_fetch: bool =
1060 get_settings_value("verify.remote_manifest_fetch").unwrap();
1061 let auto_thumbnail: bool = get_settings_value("builder.thumbnail.enabled").unwrap();
1062 let user_anchors: Option<String> = get_settings_value("trust.user_anchors").unwrap();
1063
1064 assert_eq!(
1065 remote_manifest_fetch,
1066 Verify::default().remote_manifest_fetch
1067 );
1068 assert_eq!(auto_thumbnail, BuilderSettings::default().thumbnail.enabled);
1069 assert_eq!(user_anchors, Trust::default().user_anchors);
1070
1071 // test implicit deserialization on objects
1072 let core: Core = get_settings_value("core").unwrap();
1073 let verify: Verify = get_settings_value("verify").unwrap();
1074 let builder: BuilderSettings = get_settings_value("builder").unwrap();
1075 let trust: Trust = get_settings_value("trust").unwrap();
1076
1077 assert_eq!(core, Core::default());
1078 assert_eq!(verify, Verify::default());
1079 assert_eq!(builder, BuilderSettings::default());
1080 assert_eq!(trust, Trust::default());
1081
1082 reset_default_settings().unwrap();
1083 }
1084
1085 #[test]
1086 fn test_set_val_by_direct_path() {
1087 let ts = include_bytes!("../../tests/fixtures/certs/trust/test_cert_root_bundle.pem");
1088
1089 // test updating values
1090 Settings::set_thread_local_value("core.merkle_tree_chunk_size_in_kb", 10).unwrap();
1091 Settings::set_thread_local_value("verify.remote_manifest_fetch", false).unwrap();
1092 Settings::set_thread_local_value("builder.thumbnail.enabled", false).unwrap();
1093 Settings::set_thread_local_value(
1094 "trust.user_anchors",
1095 Some(String::from_utf8(ts.to_vec()).unwrap()),
1096 )
1097 .unwrap();
1098
1099 assert_eq!(
1100 get_settings_value::<usize>("core.merkle_tree_chunk_size_in_kb").unwrap(),
1101 10
1102 );
1103 assert!(!get_settings_value::<bool>("verify.remote_manifest_fetch").unwrap());
1104 assert!(!get_settings_value::<bool>("builder.thumbnail.enabled").unwrap());
1105 assert_eq!(
1106 get_settings_value::<Option<String>>("trust.user_anchors").unwrap(),
1107 Some(String::from_utf8(ts.to_vec()).unwrap())
1108 );
1109
1110 // the current config should be different from the defaults
1111 assert_ne!(get_settings_value::<Core>("core").unwrap(), Core::default());
1112 assert_ne!(
1113 get_settings_value::<Verify>("verify").unwrap(),
1114 Verify::default()
1115 );
1116 assert_ne!(
1117 get_settings_value::<BuilderSettings>("builder").unwrap(),
1118 BuilderSettings::default()
1119 );
1120 assert!(get_settings_value::<Trust>("trust").unwrap() == Trust::default());
1121
1122 reset_default_settings().unwrap();
1123 }
1124
1125 #[cfg(feature = "file_io")]
1126 #[test]
1127 fn test_save_load() {
1128 let temp_dir = tempdirectory().unwrap();
1129 let op = crate::utils::test::temp_dir_path(&temp_dir, "sdk_config.json");
1130
1131 save_settings_as_json(&op).unwrap();
1132
1133 Settings::from_file(&op).unwrap();
1134 let settings = get_thread_local_settings();
1135
1136 assert_eq!(settings, Settings::default());
1137
1138 reset_default_settings().unwrap();
1139 }
1140
1141 #[cfg(feature = "file_io")]
1142 #[test]
1143 fn test_save_load_from_string() {
1144 let temp_dir = tempdirectory().unwrap();
1145 let op = crate::utils::test::temp_dir_path(&temp_dir, "sdk_config.json");
1146
1147 save_settings_as_json(&op).unwrap();
1148
1149 let setting_buf = std::fs::read(&op).unwrap();
1150
1151 {
1152 let settings_str: &str = &String::from_utf8_lossy(&setting_buf);
1153 Settings::from_string(settings_str, "json").map(|_| ())
1154 }
1155 .unwrap();
1156 let settings = get_thread_local_settings();
1157
1158 assert_eq!(settings, Settings::default());
1159
1160 reset_default_settings().unwrap();
1161 }
1162
1163 #[test]
1164 fn test_partial_loading() {
1165 // we support just changing the fields you are interested in changing
1166 // here is an example of incomplete structures only overriding specific
1167 // fields
1168
1169 let modified_core = toml::toml! {
1170 [core]
1171 debug = true
1172 hash_alg = "sha512"
1173 max_memory_usage = 123456
1174 }
1175 .to_string();
1176
1177 Settings::from_toml(&modified_core).unwrap();
1178
1179 // see if updated values match
1180 assert!(get_settings_value::<bool>("core.debug").unwrap());
1181 assert_eq!(
1182 get_settings_value::<String>("core.hash_alg").unwrap(),
1183 "sha512".to_string()
1184 );
1185 assert_eq!(
1186 get_settings_value::<u32>("core.max_memory_usage").unwrap(),
1187 123456u32
1188 );
1189
1190 // check a few defaults to make sure they are still there
1191 assert_eq!(
1192 get_settings_value::<bool>("builder.thumbnail.enabled").unwrap(),
1193 BuilderSettings::default().thumbnail.enabled
1194 );
1195
1196 reset_default_settings().unwrap();
1197 }
1198
1199 #[test]
1200 fn test_bad_setting() {
1201 let modified_core = toml::toml! {
1202 [core]
1203 merkle_tree_chunk_size_in_kb = true
1204 merkle_tree_max_proofs = "sha1000000"
1205 backing_store_memory_threshold_in_mb = -123456
1206 }
1207 .to_string();
1208
1209 assert!(Settings::from_toml(&modified_core).is_err());
1210
1211 reset_default_settings().unwrap();
1212 }
1213 #[test]
1214 fn test_hidden_setting() {
1215 let secret = toml::toml! {
1216 [hidden]
1217 test1 = true
1218 test2 = "hello world"
1219 test3 = 123456
1220 }
1221 .to_string();
1222
1223 Settings::from_toml(&secret).unwrap();
1224
1225 assert!(get_settings_value::<bool>("hidden.test1").unwrap());
1226 assert_eq!(
1227 get_settings_value::<String>("hidden.test2").unwrap(),
1228 "hello world".to_string()
1229 );
1230 assert_eq!(
1231 get_settings_value::<u32>("hidden.test3").unwrap(),
1232 123456u32
1233 );
1234
1235 reset_default_settings().unwrap();
1236 }
1237
1238 #[test]
1239 fn test_all_setting() {
1240 let all_settings = toml::toml! {
1241 version = 1
1242
1243 [trust]
1244
1245 [Core]
1246 debug = false
1247 hash_alg = "sha256"
1248 salt_jumbf_boxes = true
1249 prefer_bmff_merkle_tree = false
1250 compress_manifests = true
1251
1252 [Builder]
1253 prefer_box_hash = false
1254
1255 [Verify]
1256 verify_after_reading = true
1257 verify_after_sign = true
1258 verify_trust = true
1259 ocsp_fetch = false
1260 remote_manifest_fetch = true
1261 skip_ingredient_conflict_resolution = false
1262 strict_v1_validation = false
1263 }
1264 .to_string();
1265
1266 Settings::from_toml(&all_settings).unwrap();
1267
1268 reset_default_settings().unwrap();
1269 }
1270
1271 #[test]
1272 fn test_load_settings_from_sample_toml() {
1273 #[cfg(target_os = "wasi")]
1274 Settings::reset().unwrap();
1275
1276 let toml = include_bytes!("../../examples/c2pa.toml");
1277 Settings::from_toml(std::str::from_utf8(toml).unwrap()).unwrap();
1278 }
1279
1280 #[test]
1281 fn test_update_from_str_toml() {
1282 let mut settings = Settings::default();
1283
1284 // Check defaults
1285 assert!(settings.verify.verify_after_reading);
1286 assert!(settings.verify.verify_trust);
1287
1288 // Set both to false
1289 settings
1290 .update_from_str(
1291 r#"
1292 [verify]
1293 verify_after_reading = false
1294 verify_trust = false
1295 "#,
1296 "toml",
1297 )
1298 .unwrap();
1299
1300 assert!(!settings.verify.verify_after_reading);
1301 assert!(!settings.verify.verify_trust);
1302
1303 // Override: set one to true, keep other false
1304 settings
1305 .update_from_str(
1306 r#"
1307 [verify]
1308 verify_after_reading = true
1309 "#,
1310 "toml",
1311 )
1312 .unwrap();
1313
1314 assert!(settings.verify.verify_after_reading);
1315 assert!(!settings.verify.verify_trust);
1316 }
1317
1318 #[test]
1319 fn test_update_from_str_json() {
1320 let mut settings = Settings::default();
1321
1322 // Check defaults
1323 assert!(settings.verify.verify_after_reading);
1324 assert!(settings.verify.verify_trust);
1325 assert!(settings.builder.created_assertion_labels.is_none());
1326
1327 // Set both to false and set created_assertion_labels
1328 settings
1329 .update_from_str(
1330 r#"
1331 {
1332 "verify": {
1333 "verify_after_reading": false,
1334 "verify_trust": false
1335 },
1336 "builder": {
1337 "created_assertion_labels": ["c2pa.metadata"]
1338 }
1339 }
1340 "#,
1341 "json",
1342 )
1343 .unwrap();
1344
1345 assert!(!settings.verify.verify_after_reading);
1346 assert!(!settings.verify.verify_trust);
1347 assert_eq!(
1348 settings.builder.created_assertion_labels,
1349 Some(vec!["c2pa.metadata".to_string()])
1350 );
1351
1352 // Override: set one to true, keep other false
1353 settings
1354 .update_from_str(
1355 r#"
1356 {
1357 "verify": {
1358 "verify_after_reading": true
1359 }
1360 }
1361 "#,
1362 "json",
1363 )
1364 .unwrap();
1365
1366 assert!(settings.verify.verify_after_reading);
1367 assert!(!settings.verify.verify_trust);
1368 assert_eq!(
1369 settings.builder.created_assertion_labels,
1370 Some(vec!["c2pa.metadata".to_string()])
1371 );
1372
1373 // Set created_assertion_labels back to null
1374 settings
1375 .update_from_str(
1376 r#"
1377 {
1378 "builder": {
1379 "created_assertion_labels": null
1380 }
1381 }
1382 "#,
1383 "json",
1384 )
1385 .unwrap();
1386
1387 assert!(settings.verify.verify_after_reading);
1388 assert!(!settings.verify.verify_trust);
1389 assert!(settings.builder.created_assertion_labels.is_none());
1390 }
1391
1392 #[test]
1393 fn test_update_from_str_invalid() {
1394 assert!(Settings::default()
1395 .update_from_str("invalid toml { ]", "toml")
1396 .is_err());
1397 assert!(Settings::default()
1398 .update_from_str("{ invalid json }", "json")
1399 .is_err());
1400 assert!(Settings::default().update_from_str("data", "yaml").is_err());
1401 }
1402
1403 #[test]
1404 fn test_instance_with_value() {
1405 // Test builder pattern with with_value
1406 let settings = Settings::default()
1407 .with_value("verify.verify_trust", false)
1408 .unwrap()
1409 .with_value("core.merkle_tree_chunk_size_in_kb", 1024i64)
1410 .unwrap()
1411 .with_value("builder.thumbnail.enabled", false)
1412 .unwrap();
1413
1414 assert!(!settings.verify.verify_trust);
1415 assert_eq!(settings.core.merkle_tree_chunk_size_in_kb, Some(1024));
1416 assert!(!settings.builder.thumbnail.enabled);
1417 }
1418
1419 #[test]
1420 fn test_instance_set_value() {
1421 // Test mutable set_value
1422 let mut settings = Settings::default();
1423
1424 settings.set_value("verify.verify_trust", true).unwrap();
1425 settings
1426 .set_value("core.merkle_tree_chunk_size_in_kb", 2048i64)
1427 .unwrap();
1428 settings
1429 .set_value("builder.thumbnail.enabled", false)
1430 .unwrap();
1431
1432 assert!(settings.verify.verify_trust);
1433 assert_eq!(settings.core.merkle_tree_chunk_size_in_kb, Some(2048));
1434 assert!(!settings.builder.thumbnail.enabled);
1435 }
1436
1437 #[test]
1438 fn test_instance_get_value() {
1439 let mut settings = Settings::default();
1440 settings.verify.verify_trust = false;
1441 settings.core.merkle_tree_chunk_size_in_kb = Some(512);
1442
1443 // Test type inference
1444 let verify_trust: bool = settings.get_value("verify.verify_trust").unwrap();
1445 assert!(!verify_trust);
1446
1447 // Test explicit type
1448 let chunk_size = settings
1449 .get_value::<Option<usize>>("core.merkle_tree_chunk_size_in_kb")
1450 .unwrap();
1451 assert_eq!(chunk_size, Some(512));
1452 }
1453
1454 #[test]
1455 fn test_instance_methods_with_context() {
1456 // Test that instance methods work with Context
1457 use crate::Context;
1458
1459 let settings = Settings::default()
1460 .with_value("verify.verify_after_sign", true)
1461 .unwrap()
1462 .with_value("verify.verify_trust", true)
1463 .unwrap();
1464
1465 let _context = Context::new().with_settings(settings).unwrap();
1466
1467 // If we get here without errors, the integration works
1468 }
1469
1470 #[test]
1471 fn test_instance_value_error_handling() {
1472 // Test invalid type (trying to set string to bool field)
1473 let mut settings = Settings::default();
1474 let result = settings.set_value("verify.verify_trust", "not a bool");
1475 assert!(result.is_err());
1476
1477 // Test get non-existent path
1478 let settings = Settings::default();
1479 let result = settings.get_value::<bool>("does.not.exist");
1480 assert!(result.is_err());
1481
1482 // Test with_value on invalid type
1483 let result = Settings::default().with_value("verify.verify_trust", "not a bool");
1484 assert!(result.is_err());
1485 }
1486
1487 #[test]
1488 fn test_test_settings() {
1489 // Test that test_settings loads correctly
1490 let settings = test_settings();
1491
1492 // Verify it has trust anchors (test fixture includes multiple root CAs)
1493 assert!(
1494 settings.trust.trust_anchors.is_some(),
1495 "test_settings should include trust anchors"
1496 );
1497 assert!(
1498 !settings.trust.trust_anchors.as_ref().unwrap().is_empty(),
1499 "test_settings trust_anchors should not be empty"
1500 );
1501
1502 // Verify it has a signer configured
1503 assert!(
1504 settings.signer.is_some(),
1505 "test_settings should include a signer"
1506 );
1507
1508 // Verify we have a local signer with valid configuration
1509 if let Some(SignerSettings::Local { alg, .. }) = &settings.signer {
1510 // Just verify we have an algorithm set (validates the structure loaded correctly)
1511 assert!(
1512 matches!(
1513 alg,
1514 SigningAlg::Ps256
1515 | SigningAlg::Es256
1516 | SigningAlg::Es384
1517 | SigningAlg::Es512
1518 | SigningAlg::Ed25519
1519 ),
1520 "test_settings should have a valid signing algorithm"
1521 );
1522 } else {
1523 panic!("test_settings should have a Local signer configured");
1524 }
1525 }
1526
1527 #[test]
1528 fn test_builder_pattern() {
1529 // Test Settings::new() with builder pattern
1530 let settings = Settings::new()
1531 .with_json(r#"{"verify": {"verify_trust": false}}"#)
1532 .unwrap();
1533 assert!(!settings.verify.verify_trust);
1534
1535 // Test chaining with_json and with_value
1536 let settings = Settings::new()
1537 .with_json(r#"{"verify": {"verify_after_reading": false}}"#)
1538 .unwrap()
1539 .with_value("verify.verify_trust", true)
1540 .unwrap();
1541 assert!(!settings.verify.verify_after_reading);
1542 assert!(settings.verify.verify_trust);
1543
1544 // Test with_toml
1545 let settings = Settings::new()
1546 .with_toml(
1547 r#"
1548 [verify]
1549 verify_trust = false
1550 verify_after_sign = false
1551 "#,
1552 )
1553 .unwrap();
1554 assert!(!settings.verify.verify_trust);
1555 assert!(!settings.verify.verify_after_sign);
1556
1557 // Test that it doesn't update thread-locals
1558 let original = get_thread_local_settings();
1559 let _settings = Settings::new()
1560 .with_json(r#"{"verify": {"verify_trust": false}}"#)
1561 .unwrap();
1562 let after = get_thread_local_settings();
1563 // thread-local settings should be unchanged
1564 assert_eq!(
1565 original.verify.verify_trust, after.verify.verify_trust,
1566 "Builder pattern should not modify thread_local settings"
1567 );
1568 }
1569}