1use anyhow::Context;
2
3pub const MAX_STDIN_BYTES: u64 = 64 << 20;
5pub const MAX_HORIZON: usize = 4096;
7pub const MAX_CONTEXT_VALUES: usize = 1_000_000;
9
10pub fn parse_mv_contexts(val: serde_json::Value) -> anyhow::Result<Vec<Vec<Vec<f32>>>> {
17 let parsed = parse_mv_contexts_inner(val)?;
18 validate_contexts(&parsed)?;
19 Ok(parsed)
20}
21
22fn parse_mv_contexts_inner(val: serde_json::Value) -> anyhow::Result<Vec<Vec<Vec<f32>>>> {
23 match val {
24 serde_json::Value::Array(arr) if arr.is_empty() => {
25 anyhow::bail!("context must be a non-empty array")
26 }
27 serde_json::Value::Array(arr) => {
28 let first = arr.first().context("context must be a non-empty array")?;
29 if !first.is_array() {
30 let ctx = serde_json::from_value::<Vec<f32>>(serde_json::Value::Array(arr))
32 .context("context must be a JSON array of numbers")?;
33 anyhow::ensure!(!ctx.is_empty(), "context series must not be empty");
34 Ok(vec![vec![ctx]])
35 } else if first
36 .as_array()
37 .and_then(|a| a.first())
38 .map(|v| v.is_array())
39 .unwrap_or(false)
40 {
41 let out: Vec<Vec<Vec<f32>>> = arr
43 .into_iter()
44 .enumerate()
45 .map(|(i, batch_item)| {
46 let variates = serde_json::from_value::<Vec<Vec<f32>>>(batch_item)
47 .with_context(|| {
48 format!("context[{i}] must be an array of variate arrays")
49 })?;
50 anyhow::ensure!(!variates.is_empty(), "context[{i}] must not be empty");
51 for (vi, series) in variates.iter().enumerate() {
52 anyhow::ensure!(
53 !series.is_empty(),
54 "context[{i}][{vi}] must not be empty"
55 );
56 }
57 Ok(variates)
58 })
59 .collect::<anyhow::Result<_>>()?;
60 anyhow::ensure!(!out.is_empty(), "context must be a non-empty array");
61 Ok(out)
62 } else {
63 let out: Vec<Vec<Vec<f32>>> = arr
65 .into_iter()
66 .enumerate()
67 .map(|(i, v)| {
68 let series = serde_json::from_value::<Vec<f32>>(v)
69 .with_context(|| format!("context[{i}] must be an array of numbers"))?;
70 anyhow::ensure!(!series.is_empty(), "context[{i}] must not be empty");
71 Ok(vec![series])
72 })
73 .collect::<anyhow::Result<_>>()?;
74 anyhow::ensure!(!out.is_empty(), "context must be a non-empty array");
75 Ok(out)
76 }
77 }
78 _ => anyhow::bail!("context must be a JSON array"),
79 }
80}
81
82pub fn validate_contexts(contexts: &[Vec<Vec<f32>>]) -> anyhow::Result<()> {
84 anyhow::ensure!(!contexts.is_empty(), "context must be a non-empty array");
85 let mut total: usize = 0;
86 for (bi, batch) in contexts.iter().enumerate() {
87 anyhow::ensure!(!batch.is_empty(), "context[{bi}] must not be empty");
88 for (vi, series) in batch.iter().enumerate() {
89 anyhow::ensure!(!series.is_empty(), "context[{bi}][{vi}] must not be empty");
90 total = total.saturating_add(series.len());
91 anyhow::ensure!(
92 total <= MAX_CONTEXT_VALUES,
93 "context too large — got {total} values, max is {MAX_CONTEXT_VALUES}"
94 );
95 for &v in series {
96 anyhow::ensure!(
97 v.is_finite(),
98 "context[{bi}][{vi}] must contain only finite numbers"
99 );
100 }
101 }
102 }
103 Ok(())
104}
105
106pub fn parse_horizon(val: &serde_json::Value) -> anyhow::Result<usize> {
108 let horizon = val
109 .get("horizon")
110 .and_then(|v| v.as_u64())
111 .context("horizon must be a positive integer")? as usize;
112 validate_horizon(horizon)?;
113 Ok(horizon)
114}
115
116pub fn validate_horizon(horizon: usize) -> anyhow::Result<()> {
118 anyhow::ensure!(horizon >= 1, "horizon must be a positive integer");
119 anyhow::ensure!(
120 horizon <= MAX_HORIZON,
121 "horizon too large — got {horizon}, max is {MAX_HORIZON}"
122 );
123 Ok(())
124}
125
126pub fn parse_matrix(val: &serde_json::Value) -> anyhow::Result<Vec<Vec<f32>>> {
128 let mat: Vec<Vec<f32>> =
129 serde_json::from_value(val.clone()).context("expected a 2D JSON array")?;
130 anyhow::ensure!(!mat.is_empty(), "matrix must be a non-empty array");
131 let cols = mat[0].len();
132 anyhow::ensure!(cols > 0, "matrix rows must not be empty");
133 anyhow::ensure!(
134 mat.len() * cols <= MAX_CONTEXT_VALUES,
135 "matrix too large — max is {MAX_CONTEXT_VALUES} values"
136 );
137 for (ri, row) in mat.iter().enumerate() {
138 anyhow::ensure!(row.len() == cols, "row {ri} has inconsistent width");
139 for &v in row {
140 anyhow::ensure!(
141 v.is_finite(),
142 "matrix row {ri} must contain only finite numbers"
143 );
144 }
145 }
146 Ok(mat)
147}
148
149pub fn softmax(logits: &[f32]) -> Vec<f32> {
151 if logits.is_empty() {
152 return Vec::new();
153 }
154 let max = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
155 let exps: Vec<f32> = logits.iter().map(|&v| (v - max).exp()).collect();
157 let sum: f32 = exps.iter().sum();
158 if sum == 0.0 || !sum.is_finite() {
159 return vec![1.0 / logits.len() as f32; logits.len()];
160 }
161 exps.into_iter().map(|v| v / sum).collect()
162}
163
164pub fn read_stdin_limited() -> anyhow::Result<String> {
166 use std::io::Read;
167 let mut buf = String::new();
168 std::io::stdin()
169 .take(MAX_STDIN_BYTES + 1)
170 .read_to_string(&mut buf)
171 .context("read stdin")?;
172 anyhow::ensure!(
173 (buf.len() as u64) <= MAX_STDIN_BYTES,
174 "stdin input too large — max is {MAX_STDIN_BYTES} bytes"
175 );
176 Ok(buf)
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn rejects_empty_contexts() {
185 assert!(parse_mv_contexts(serde_json::json!([])).is_err());
186 assert!(parse_mv_contexts(serde_json::json!([[]])).is_err());
187 assert!(parse_mv_contexts(serde_json::json!([[[]]])).is_err());
188 }
189
190 #[test]
191 fn rejects_non_finite_context() {
192 let nan = f64::NAN;
193 let v = serde_json::json!([1.0, 2.0]);
195 let mut ctx = parse_mv_contexts(v).unwrap();
196 ctx[0][0][0] = f32::from_bits(0x7fc0_0000);
197 assert!(validate_contexts(&ctx).is_err());
198 let _ = nan;
199 }
200
201 #[test]
202 fn horizon_bounds() {
203 assert!(validate_horizon(0).is_err());
204 assert!(validate_horizon(1).is_ok());
205 assert!(validate_horizon(MAX_HORIZON).is_ok());
206 assert!(validate_horizon(MAX_HORIZON + 1).is_err());
207 }
208
209 #[test]
210 fn matrix_validation() {
211 assert!(parse_matrix(&serde_json::json!([])).is_err());
212 assert!(parse_matrix(&serde_json::json!([[1.0], [2.0, 3.0]])).is_err());
213 assert!(parse_matrix(&serde_json::json!([[1.0, 2.0]])).is_ok());
214 assert_eq!(softmax(&[]).len(), 0);
215 let p = softmax(&[1.0, 2.0]);
216 assert!((p.iter().sum::<f32>() - 1.0).abs() < 1e-5);
217 }
218}