automorph 0.2.0

Derive macros for bidirectional Automerge-Rust struct synchronization
Documentation
//! Configuration for Automorph behavior.
//!
//! This module provides configuration options that can be set globally or per-thread.
//!
//! # Collection Size Limits
//!
//! By default, Automorph limits collections to `DEFAULT_MAX_COLLECTION_SIZE` (10 million)
//! elements to prevent memory exhaustion from malicious documents.
//!
//! You can override this limit globally or for a specific scope:
//!
//! ```rust
//! use automorph::config;
//!
//! // Set a global limit (affects all threads until changed)
//! config::set_max_collection_size(1_000_000);
//!
//! // Or use a scoped limit that automatically restores the previous value
//! config::with_max_collection_size(100, || {
//!     // Within this closure, max collection size is 100
//!     // After the closure returns, the previous limit is restored
//! });
//! ```

use std::cell::Cell;

/// Default maximum number of elements allowed when loading collections.
/// This prevents memory exhaustion from malicious documents that claim
/// to have billions of elements.
///
/// Default value: 10,000,000 elements
pub const DEFAULT_MAX_COLLECTION_SIZE: usize = 10_000_000;

thread_local! {
    static MAX_COLLECTION_SIZE: Cell<usize> = const { Cell::new(DEFAULT_MAX_COLLECTION_SIZE) };
}

/// Gets the current maximum collection size limit.
///
/// This returns the thread-local limit if set, otherwise the default limit.
///
/// # Example
///
/// ```rust
/// use automorph::config;
///
/// let limit = config::max_collection_size();
/// assert_eq!(limit, config::DEFAULT_MAX_COLLECTION_SIZE);
/// ```
pub fn max_collection_size() -> usize {
    MAX_COLLECTION_SIZE.with(|cell| cell.get())
}

/// Sets the maximum collection size limit for the current thread.
///
/// This affects all subsequent collection loads on this thread until changed.
///
/// # Arguments
///
/// * `size` - The new maximum collection size. Set to `usize::MAX` to disable the limit.
///
/// # Example
///
/// ```rust
/// use automorph::config;
///
/// // Allow larger collections
/// config::set_max_collection_size(100_000_000);
///
/// // Or disable the limit entirely (use with caution!)
/// config::set_max_collection_size(usize::MAX);
/// ```
///
/// # Safety
///
/// Setting a very high or unlimited value (`usize::MAX`) may expose your application
/// to denial-of-service attacks if loading untrusted documents.
pub fn set_max_collection_size(size: usize) {
    MAX_COLLECTION_SIZE.with(|cell| cell.set(size));
}

/// Executes a closure with a temporary collection size limit.
///
/// The previous limit is automatically restored after the closure returns,
/// even if it panics.
///
/// # Arguments
///
/// * `size` - The temporary maximum collection size
/// * `f` - The closure to execute with the temporary limit
///
/// # Example
///
/// ```rust
/// use automorph::config;
///
/// // Temporarily allow very small collections (e.g., for testing)
/// config::with_max_collection_size(10, || {
///     // In this scope, collections are limited to 10 elements
/// });
/// // Previous limit is restored here
/// ```
pub fn with_max_collection_size<F, R>(size: usize, f: F) -> R
where
    F: FnOnce() -> R,
{
    struct RestoreOnDrop(usize);

    impl Drop for RestoreOnDrop {
        fn drop(&mut self) {
            set_max_collection_size(self.0);
        }
    }

    let previous = max_collection_size();
    let _guard = RestoreOnDrop(previous);
    set_max_collection_size(size);
    f()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_limit() {
        // Reset to default
        set_max_collection_size(DEFAULT_MAX_COLLECTION_SIZE);
        assert_eq!(max_collection_size(), DEFAULT_MAX_COLLECTION_SIZE);
    }

    #[test]
    fn test_set_limit() {
        let original = max_collection_size();

        set_max_collection_size(100);
        assert_eq!(max_collection_size(), 100);

        // Restore
        set_max_collection_size(original);
    }

    #[test]
    fn test_with_limit_scope() {
        let original = max_collection_size();

        with_max_collection_size(50, || {
            assert_eq!(max_collection_size(), 50);
        });

        // Should be restored
        assert_eq!(max_collection_size(), original);
    }

    #[test]
    fn test_with_limit_restores_on_panic() {
        let original = max_collection_size();

        let result = std::panic::catch_unwind(|| {
            with_max_collection_size(25, || {
                panic!("test panic");
            });
        });

        assert!(result.is_err());
        // Should be restored even after panic
        assert_eq!(max_collection_size(), original);
    }
}