Skip to main content

ryg_rans_rs_cli/
limits.rs

1//! # Resource limits for the ryg-rans CLI
2//!
3//! Central `Limits` type that controls all resource bounds.
4//! Every command must respect these limits.  No command may bypass limits
5//! because it uses stdin rather than a file.
6//!
7//! Limits are enforced during reading, not after.  All accumulation uses
8//! checked arithmetic.
9
10use crate::error::{AppError, ResourceLimitError};
11use std::time::Duration;
12
13/// Default maximum input size (16 GiB).
14pub const DEFAULT_MAX_INPUT: u64 = 16 * 1024 * 1024 * 1024;
15
16/// Default maximum output size (16 GiB).
17pub const DEFAULT_MAX_OUTPUT: u64 = 16 * 1024 * 1024 * 1024;
18
19/// Default maximum block size (1 MiB).
20pub const DEFAULT_BLOCK_SIZE: u32 = 1024 * 1024;
21
22/// Hard maximum block size (64 MiB).
23pub const HARD_MAX_BLOCK_SIZE: u32 = 64 * 1024 * 1024;
24
25/// Default maximum payload per block (same as block size).
26pub const DEFAULT_MAX_PAYLOAD: u32 = 1024 * 1024;
27
28/// Default maximum model size (2 KB — 256 symbols × 5 bytes + 2).
29pub const DEFAULT_MAX_MODEL: u32 = 2048;
30
31/// Default maximum blocks (1 million).
32pub const DEFAULT_MAX_BLOCKS: u64 = 1_000_000;
33
34/// Default maximum trace symbols.
35pub const DEFAULT_MAX_TRACE_SYMBOLS: u64 = 256;
36
37/// Default maximum oracle output bytes (64 MB).
38pub const DEFAULT_MAX_ORACLE_OUTPUT: u64 = 64 * 1024 * 1024;
39
40/// Default oracle timeout.
41pub const DEFAULT_ORACLE_TIMEOUT: Duration = Duration::from_secs(60);
42
43/// Central resource limits.
44#[derive(Debug, Clone)]
45pub struct Limits {
46    /// Maximum input bytes to read.
47    pub max_input_bytes: u64,
48    /// Maximum output bytes to produce.
49    pub max_output_bytes: u64,
50    /// Maximum bytes per uncompressed block.
51    pub max_block_bytes: u32,
52    /// Maximum bytes per compressed payload.
53    pub max_payload_bytes: u32,
54    /// Maximum bytes per model encoding.
55    pub max_model_bytes: u32,
56    /// Maximum number of blocks.
57    pub max_blocks: u64,
58    /// Maximum trace symbols to emit.
59    pub max_trace_symbols: u64,
60    /// Maximum bytes to capture from oracle output.
61    pub max_oracle_output_bytes: u64,
62    /// Maximum wall-clock time for oracle execution.
63    pub oracle_timeout: Duration,
64}
65
66impl Default for Limits {
67    fn default() -> Self {
68        Self {
69            max_input_bytes: DEFAULT_MAX_INPUT,
70            max_output_bytes: DEFAULT_MAX_OUTPUT,
71            max_block_bytes: DEFAULT_BLOCK_SIZE,
72            max_payload_bytes: DEFAULT_MAX_PAYLOAD,
73            max_model_bytes: DEFAULT_MAX_MODEL,
74            max_blocks: DEFAULT_MAX_BLOCKS,
75            max_trace_symbols: DEFAULT_MAX_TRACE_SYMBOLS,
76            max_oracle_output_bytes: DEFAULT_MAX_ORACLE_OUTPUT,
77            oracle_timeout: DEFAULT_ORACLE_TIMEOUT,
78        }
79    }
80}
81
82impl Limits {
83    /// Check that a block's declared uncompressed length is within limits.
84    pub fn check_block_size(&self, size: u32) -> Result<(), AppError> {
85        if size > self.max_block_bytes {
86            return Err(AppError::ResourceLimit(ResourceLimitError {
87                detail: "block size exceeds limit".into(),
88                limit: self.max_block_bytes as u64,
89                requested: size as u64,
90            }));
91        }
92        Ok(())
93    }
94
95    /// Check that a block's declared payload length is within limits.
96    pub fn check_payload_size(&self, size: u32) -> Result<(), AppError> {
97        if size > self.max_payload_bytes {
98            return Err(AppError::ResourceLimit(ResourceLimitError {
99                detail: "payload size exceeds limit".into(),
100                limit: self.max_payload_bytes as u64,
101                requested: size as u64,
102            }));
103        }
104        Ok(())
105    }
106
107    /// Check that a model length is within limits.
108    pub fn check_model_size(&self, size: u32) -> Result<(), AppError> {
109        if size > self.max_model_bytes {
110            return Err(AppError::ResourceLimit(ResourceLimitError {
111                detail: "model size exceeds limit".into(),
112                limit: self.max_model_bytes as u64,
113                requested: size as u64,
114            }));
115        }
116        Ok(())
117    }
118
119    /// Check that the block count does not exceed the limit.
120    pub fn check_block_count(&self, count: u64) -> Result<(), AppError> {
121        if count > self.max_blocks {
122            return Err(AppError::ResourceLimit(ResourceLimitError {
123                detail: "block count exceeds limit".into(),
124                limit: self.max_blocks,
125                requested: count,
126            }));
127        }
128        Ok(())
129    }
130
131    /// Check that adding `additional` to `current` output does not exceed the limit.
132    /// Uses checked arithmetic.
133    pub fn check_output_total(&self, current: u64, additional: u64) -> Result<u64, AppError> {
134        let new_total = current.checked_add(additional).ok_or_else(|| {
135            AppError::ResourceLimit(ResourceLimitError {
136                detail: "output total overflow".into(),
137                limit: self.max_output_bytes,
138                requested: current.saturating_add(additional),
139            })
140        })?;
141        if new_total > self.max_output_bytes {
142            return Err(AppError::ResourceLimit(ResourceLimitError {
143                detail: "output total exceeds limit".into(),
144                limit: self.max_output_bytes,
145                requested: new_total,
146            }));
147        }
148        Ok(new_total)
149    }
150
151    /// Parse a human-readable size string (e.g., "1MiB", "64KiB", "1024").
152    /// Supports: KiB, MiB, GiB (1024-based).  No suffix = bytes.
153    pub fn parse_size(s: &str) -> Result<u64, String> {
154        let s = s.trim();
155        if s.is_empty() {
156            return Err("empty size string".into());
157        }
158
159        // Find the numeric prefix and optional suffix
160        let num_end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
161        let num_part = &s[..num_end];
162        let suffix = s[num_end..].trim();
163
164        let value: u64 = num_part
165            .parse()
166            .map_err(|_| format!("invalid number: '{}'", num_part))?;
167
168        match suffix.to_lowercase().as_str() {
169            "" | "b" => Ok(value),
170            "kib" | "ki" => value.checked_mul(1024).ok_or("overflow".into()),
171            "mib" | "mi" => value.checked_mul(1024 * 1024).ok_or("overflow".into()),
172            "gib" | "gi" => value
173                .checked_mul(1024 * 1024 * 1024)
174                .ok_or("overflow".into()),
175            _ => Err(format!("unknown size suffix: '{}'", suffix)),
176        }
177    }
178}