Skip to main content

salvor_runtime/
labels.rs

1//! Sanity bounds for run labels.
2//!
3//! A label is a short correlation tag on a run (a build id, an environment
4//! name), not a payload: [`Event::RunStarted::labels`](salvor_core::Event)
5//! carries whatever a caller supplies, with no cap of its own, because the
6//! event type is a durable record, not a policy enforcer (an old or foreign
7//! log is trusted and replayed exactly as recorded, whatever it holds). The
8//! bounds live here instead, checked at every point a run is actually
9//! created: [`crate::RunCtx::begin`] going live, the server's `POST
10//! /v1/runs`, and the client-run event append that carries a fresh
11//! `RunStarted`. One shared function means the three surfaces cannot drift.
12
13use std::collections::BTreeMap;
14
15/// The most labels one run may carry.
16pub const MAX_LABELS: usize = 16;
17
18/// The longest a label key may be, in bytes.
19pub const MAX_LABEL_KEY_LEN: usize = 64;
20
21/// The longest a label value may be, in bytes.
22pub const MAX_LABEL_VALUE_LEN: usize = 256;
23
24/// Checks `labels` against the bounds documented at module level.
25///
26/// # Errors
27///
28/// A human-readable description of the first violation found: too many
29/// labels, or a key/value over its length cap. Checked in that order, so a
30/// caller with both problems sees the count violation first.
31pub fn validate_labels(labels: &BTreeMap<String, String>) -> Result<(), String> {
32    if labels.len() > MAX_LABELS {
33        return Err(format!(
34            "a run may carry at most {MAX_LABELS} labels, got {}",
35            labels.len()
36        ));
37    }
38    for (key, value) in labels {
39        if key.len() > MAX_LABEL_KEY_LEN {
40            return Err(format!(
41                "label key `{key}` is {} bytes, over the {MAX_LABEL_KEY_LEN}-byte cap",
42                key.len()
43            ));
44        }
45        if value.len() > MAX_LABEL_VALUE_LEN {
46            return Err(format!(
47                "label value for key `{key}` is {} bytes, over the {MAX_LABEL_VALUE_LEN}-byte cap",
48                value.len()
49            ));
50        }
51    }
52    Ok(())
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    fn map(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
60        pairs
61            .iter()
62            .map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
63            .collect()
64    }
65
66    #[test]
67    fn empty_and_small_label_sets_are_valid() {
68        assert!(validate_labels(&BTreeMap::new()).is_ok());
69        assert!(validate_labels(&map(&[("build", "42"), ("env", "prod")])).is_ok());
70    }
71
72    #[test]
73    fn more_than_the_cap_is_rejected() {
74        let labels: BTreeMap<String, String> = (0..=MAX_LABELS)
75            .map(|i| (format!("k{i}"), "v".to_owned()))
76            .collect();
77        let err = validate_labels(&labels).expect_err("over the cap must be rejected");
78        assert!(err.contains("at most 16 labels"), "{err}");
79    }
80
81    #[test]
82    fn exactly_the_cap_is_valid() {
83        let labels: BTreeMap<String, String> = (0..MAX_LABELS)
84            .map(|i| (format!("k{i}"), "v".to_owned()))
85            .collect();
86        assert!(validate_labels(&labels).is_ok());
87    }
88
89    #[test]
90    fn an_oversized_key_is_rejected() {
91        let long_key = "k".repeat(MAX_LABEL_KEY_LEN + 1);
92        let err =
93            validate_labels(&map(&[(&long_key, "v")])).expect_err("a long key must be rejected");
94        assert!(err.contains("over the 64-byte cap"), "{err}");
95    }
96
97    #[test]
98    fn an_oversized_value_is_rejected() {
99        let long_value = "v".repeat(MAX_LABEL_VALUE_LEN + 1);
100        let err = validate_labels(&map(&[("k", &long_value)]))
101            .expect_err("a long value must be rejected");
102        assert!(err.contains("over the 256-byte cap"), "{err}");
103    }
104
105    #[test]
106    fn keys_and_values_exactly_at_the_cap_are_valid() {
107        let key = "k".repeat(MAX_LABEL_KEY_LEN);
108        let value = "v".repeat(MAX_LABEL_VALUE_LEN);
109        assert!(validate_labels(&map(&[(&key, &value)])).is_ok());
110    }
111}