Skip to main content

secretspec_ipc/
revision.rs

1//! Opaque, non-secret revision metadata (0.21+).
2
3use crate::error::{Error, Result};
4use serde::{Deserialize, Deserializer, Serialize};
5
6/// A provider-defined identity and generation token, or a resolver-derived
7/// token for the logical value. Equality is meaningful; ordering is not.
8///
9/// Providers must derive this only from non-secret identity/version metadata
10/// associated with the returned value, never from secret bytes or lease handles.
11/// Syntax validation cannot establish that an endpoint honored this contract.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
13#[serde(transparent)]
14pub struct Revision(String);
15
16impl Revision {
17    /// Accept a bounded ASCII token without echoing rejected input.
18    pub fn new(value: String) -> Result<Self> {
19        if value.is_empty()
20            || value.len() > 256
21            || !value
22                .bytes()
23                .all(|b| b.is_ascii_alphanumeric() || b"._:-".contains(&b))
24        {
25            return Err(Error::Protocol("invalid revision token"));
26        }
27        Ok(Self(value))
28    }
29
30    pub fn as_str(&self) -> &str {
31        &self.0
32    }
33}
34
35impl<'de> Deserialize<'de> for Revision {
36    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
37        // Avoid Serde's string visitor diagnostics, which can echo a wrong-type
38        // input. Even malformed metadata must not appear in error messages.
39        let value = serde_json::Value::deserialize(deserializer)?;
40        match value {
41            serde_json::Value::String(value) => {
42                Self::new(value).map_err(|_| serde::de::Error::custom("invalid revision token"))
43            }
44            _ => Err(serde::de::Error::custom("invalid revision token")),
45        }
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use crate::protocol::{provider, resolver};
53    use serde_json::json;
54
55    #[test]
56    fn revisions_are_optional_on_old_single_batch_and_resolver_results() {
57        for revision in [None, Some(json!(null)), Some(json!("provider:version-1"))] {
58            let expected = revision.as_ref().and_then(|v| v.as_str());
59            let mut found = json!({"status":"found", "value":"value", "expires_at_unix_ms":null});
60            if let Some(revision) = &revision {
61                found["revision"] = revision.clone();
62            }
63            let single: provider::GetResult = serde_json::from_value(found.clone()).unwrap();
64            let provider::GetResult::Found {
65                revision: ref actual,
66                ..
67            } = single
68            else {
69                panic!()
70            };
71            assert_eq!(actual.as_ref().map(Revision::as_str), expected);
72            found["name"] = json!("TOKEN");
73            let batch: provider::NamedGetResult = serde_json::from_value(found).unwrap();
74            assert_eq!(batch.outcome, single);
75            for representation in ["value", "path"] {
76                let mut resolved = json!({"status":"resolved", "representation":representation,
77                    "value":"value", "path":"/tmp/value", "path_lease_id":"lease",
78                    "source":"provider", "expires_at_unix_ms":null, "refresh_at_unix_ms":null});
79                if let Some(revision) = &revision {
80                    resolved["revision"] = revision.clone();
81                }
82                let result: resolver::GetResult = serde_json::from_value(resolved).unwrap();
83                let actual = match result {
84                    resolver::GetResult::Value(v) => v.revision,
85                    resolver::GetResult::Path(v) => v.revision,
86                    _ => panic!(),
87                };
88                assert_eq!(actual.as_ref().map(Revision::as_str), expected);
89            }
90        }
91    }
92
93    #[test]
94    fn malformed_revision_diagnostics_do_not_echo_input() {
95        for bad in [
96            json!("CANARY/secret"),
97            json!({"CANARY":"secret"}),
98            json!(""),
99            json!("x".repeat(257)),
100            json!("é"),
101        ] {
102            let error = serde_json::from_value::<Revision>(bad)
103                .unwrap_err()
104                .to_string();
105            assert_eq!(error, "invalid revision token");
106        }
107    }
108}