//! Security limit constants for rpdfium.
//!
//! These constants protect against denial-of-service attacks from malicious PDFs.
//! Values are derived from PDFium's production-tested constants.
//!
//! Limits are divided into **hard limits** (not configurable, enforced unconditionally)
//! and **soft limits** (adjustable via [`OpenOptions`](crate::OpenOptions) for specialized use cases).

// ---------------------------------------------------------------------------
// Hard Limits (Not Configurable)
// ---------------------------------------------------------------------------

/// Maximum recursion depth for reference resolution, page tree traversal,
/// Form XObject nesting, and other recursive structures.
///
/// PDFium: `kParserMaxRecursionDepth = 64` (in `cpdf_object_walker.h`, etc.)
pub const MAX_RECURSION_DEPTH: u32 = 64;

/// Maximum object number in the cross-reference table.
///
/// PDFium: `kMaxObjectNumber = 1 << 24` (in `cpdf_cross_ref_table.h`)
pub const MAX_OBJECT_NUMBER: u32 = 24_000_000;

/// Maximum number of filters in a decode filter chain.
///
/// Prevents excessive chained decompression.
pub const MAX_FILTER_CHAIN_LENGTH: u32 = 32;

/// Maximum image dimension (width or height) in pixels.
///
/// PDFium: `kMaxImageDimension = 65535` (in `cpdf_dib.h`)
pub const MAX_IMAGE_DIMENSION: u32 = 65_535;

// ---------------------------------------------------------------------------
// Soft Limits (Configurable via OpenOptions)
// ---------------------------------------------------------------------------

/// Maximum decompressed stream size (default: 256 MB).
///
/// Prevents decompression bombs.
pub const DEFAULT_MAX_DECOMPRESSED_STREAM_SIZE: u64 = 256 * 1024 * 1024;

/// Maximum compression ratio before aborting decompression (default: 1000:1).
///
/// A ratio exceeding this strongly indicates a zip bomb.
pub const DEFAULT_MAX_COMPRESSION_RATIO: u32 = 1_000;

/// Maximum content stream operators per page (default: 10,000,000).
///
/// Prevents excessive content stream processing.
pub const DEFAULT_MAX_OPERATORS_PER_PAGE: u64 = 10_000_000;

/// Maximum distance to scan for `endstream` keyword when `/Length` is
/// missing or incorrect (default: 16 MB).
pub const DEFAULT_MAX_ENDSTREAM_SCAN_DISTANCE: u64 = 16 * 1024 * 1024;

/// Returns true if `f` is close enough to zero to be treated as zero.
///
/// Matches upstream `FXSYS_IsFloatZero` threshold of 0.0001.
pub fn is_float_zero(f: f64) -> bool {
    f.abs() < 0.0001
}

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

    #[test]
    #[allow(clippy::assertions_on_constants)]
    fn hard_limits_are_positive() {
        assert!(MAX_RECURSION_DEPTH > 0);
        assert!(MAX_OBJECT_NUMBER > 0);
        assert!(MAX_FILTER_CHAIN_LENGTH > 0);
        assert!(MAX_IMAGE_DIMENSION > 0);
    }

    #[test]
    #[allow(clippy::assertions_on_constants)]
    fn soft_limits_are_positive() {
        assert!(DEFAULT_MAX_DECOMPRESSED_STREAM_SIZE > 0);
        assert!(DEFAULT_MAX_COMPRESSION_RATIO > 0);
        assert!(DEFAULT_MAX_OPERATORS_PER_PAGE > 0);
        assert!(DEFAULT_MAX_ENDSTREAM_SCAN_DISTANCE > 0);
    }

    #[test]
    #[allow(clippy::assertions_on_constants)]
    fn max_object_number_matches_pdfium() {
        // PDFium uses kMaxObjectNumber = 1 << 24 = 16_777_216
        // We use 24_000_000 which is slightly larger for compatibility.
        assert!(MAX_OBJECT_NUMBER >= (1 << 24));
    }

    #[test]
    fn test_default_decompressed_stream_size_is_256mb() {
        assert_eq!(DEFAULT_MAX_DECOMPRESSED_STREAM_SIZE, 268_435_456);
    }

    #[test]
    fn test_default_endstream_scan_distance_is_16mb() {
        assert_eq!(DEFAULT_MAX_ENDSTREAM_SCAN_DISTANCE, 16_777_216);
    }

    #[test]
    fn test_is_float_zero_positive_small() {
        assert!(is_float_zero(0.00001));
        assert!(is_float_zero(0.00009));
    }

    #[test]
    fn test_is_float_zero_negative_small() {
        assert!(is_float_zero(-0.00001));
        assert!(is_float_zero(-0.00009));
    }

    #[test]
    fn test_is_float_zero_boundary() {
        // At exactly 0.0001 — NOT considered zero (uses strict <)
        assert!(!is_float_zero(0.0001));
        assert!(!is_float_zero(-0.0001));
        // Just below
        assert!(is_float_zero(0.00009999));
        // Clearly not zero
        assert!(!is_float_zero(1.0));
        assert!(!is_float_zero(-1.0));
    }
}
