Skip to main content

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}