secretspec_ipc/
revision.rs1use crate::error::{Error, Result};
4use serde::{Deserialize, Deserializer, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
13#[serde(transparent)]
14pub struct Revision(String);
15
16impl Revision {
17 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 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}