wash_lib/
id.rs

1//! Types and tools for basic validation of seeds and IDs used in configuration
2
3use std::{convert::AsRef, fmt::Display, ops::Deref, str::FromStr};
4
5use anyhow::{bail, Result};
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9/// An error type describing the types of errors when parsing an ID
10#[derive(Error, Debug, Eq, PartialEq)]
11pub enum ParseError {
12    /// The key is the wrong type of ID or seed
13    #[error(r#"found the prefix "{found}", but expected "{expected}""#)]
14    InvalidKeyType { found: String, expected: String },
15    /// The key does not have the proper length
16    #[error("the key should be {expected} characters, but was {found} characters")]
17    InvalidLength { found: usize, expected: usize },
18}
19
20/// A module (i.e. Component) ID
21pub type ModuleId = Id<'M'>;
22/// A host ID
23pub type ServerId = Id<'N'>;
24/// A service (i.e. Provider) ID
25pub type ServiceId = Id<'V'>;
26/// A private key for a server
27pub type ClusterSeed = Seed<'C'>;
28
29/// A wrapper around specific ID types. This is not meant to be a full nkey, but simple validation
30/// for use in serialized/deserialized types
31#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
32pub struct Id<const PREFIX: char>(String);
33
34impl<const PREFIX: char> FromStr for Id<PREFIX> {
35    type Err = ParseError;
36
37    fn from_str(s: &str) -> Result<Self, Self::Err> {
38        Ok(Self(parse(s, PREFIX, false)?))
39    }
40}
41
42impl<const PREFIX: char> AsRef<str> for Id<PREFIX> {
43    fn as_ref(&self) -> &str {
44        self.0.as_ref()
45    }
46}
47
48impl<const PREFIX: char> Deref for Id<PREFIX> {
49    type Target = str;
50
51    fn deref(&self) -> &Self::Target {
52        &self.0
53    }
54}
55
56impl<const PREFIX: char> Display for Id<PREFIX> {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        self.0.fmt(f)
59    }
60}
61
62impl<const PREFIX: char> Id<PREFIX> {
63    /// Converts the wrapped key back into a plain string
64    #[must_use]
65    pub fn into_string(self) -> String {
66        self.0
67    }
68
69    #[must_use]
70    pub fn prefix() -> char {
71        PREFIX
72    }
73}
74
75/// A wrapper around specific seed types. This is not meant to be a full nkey, but simple validation
76/// for use in serialized/deserialized types
77#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
78pub struct Seed<const PREFIX: char>(String);
79
80impl<const PREFIX: char> Display for Seed<PREFIX> {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        // NOTE: We may want to make this not print the key in the future (maybe by only
83        // implementing ToString rather than display)
84        self.0.fmt(f)
85    }
86}
87
88impl<const PREFIX: char> Default for Seed<PREFIX> {
89    fn default() -> Self {
90        Self(Default::default())
91    }
92}
93
94impl<const PREFIX: char> AsRef<str> for Seed<PREFIX> {
95    fn as_ref(&self) -> &str {
96        self.0.as_ref()
97    }
98}
99
100impl<const PREFIX: char> Deref for Seed<PREFIX> {
101    type Target = str;
102
103    fn deref(&self) -> &Self::Target {
104        &self.0
105    }
106}
107
108impl<const PREFIX: char> FromStr for Seed<PREFIX> {
109    type Err = ParseError;
110
111    fn from_str(s: &str) -> Result<Self, Self::Err> {
112        Ok(Self(parse(s, PREFIX, true)?))
113    }
114}
115
116impl<const PREFIX: char> Seed<PREFIX> {
117    /// Converts the wrapped key back into a plain string
118    #[must_use]
119    pub fn into_string(self) -> String {
120        self.0
121    }
122
123    #[must_use]
124    pub fn prefix() -> char {
125        PREFIX
126    }
127}
128
129fn parse(value: &str, prefix: char, is_seed: bool) -> Result<String, ParseError> {
130    let (len, prefix) = if is_seed {
131        (58, format!("S{prefix}"))
132    } else {
133        (56, prefix.to_string())
134    };
135
136    let count = value.chars().count();
137    if count != len {
138        return Err(ParseError::InvalidLength {
139            found: count,
140            expected: len,
141        });
142    }
143
144    if value.starts_with(&prefix) {
145        Ok(value.to_string())
146    } else {
147        Err(ParseError::InvalidKeyType {
148            found: value.chars().take(prefix.chars().count()).collect(),
149            expected: prefix,
150        })
151    }
152}
153
154// Check if the contract ID parameter is a 56 character key and suggest that the user
155// give the contract ID instead
156//
157// NOTE: `len` is ok here because keys are only ascii characters that take up a single
158// byte.
159pub fn validate_contract_id(contract_id: &str) -> Result<()> {
160    if contract_id.len() == 56
161        && contract_id
162            .chars()
163            .all(|c| c.is_ascii_digit() || c.is_ascii_uppercase())
164    {
165        bail!("It looks like you used a Component or Provider ID (e.g. VABC...) instead of a contract ID (e.g. wasmcloud:httpserver)")
166    } else {
167        Ok(())
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use test_case::test_case;
175
176    #[test_case(
177		"SC00000000000000000000000000000000000000000000000000000000", 'C', true
178		=> Ok("SC00000000000000000000000000000000000000000000000000000000".to_string());
179		"valid cluster seed")]
180    #[test_case(
181		"SC000000000000000000000000000000000000000000000000", 'C', true
182		=> Err(ParseError::InvalidLength { found: 50, expected: 58 });
183		"short cluster seed")]
184    #[test_case(
185		"SM00000000000000000000000000000000000000000000000000000000", 'C', true
186		=> Err(ParseError::InvalidKeyType { expected: "SC".to_string(), found: "SM".to_string() });
187		"cluster seed has wrong prefix")]
188    #[test_case(
189		"M0000000000000000000000000000000000000000000000000000000", 'M', false
190		=> Ok("M0000000000000000000000000000000000000000000000000000000".to_string());
191		"valid module id")]
192    #[test_case(
193		"M0000000000000000000000000000000000000000000000000", 'M', false
194		=> Err(ParseError::InvalidLength { found: 50, expected: 56 });
195		"short module id")]
196    #[test_case(
197		"V0000000000000000000000000000000000000000000000000000000", 'M', false
198		=> Err(ParseError::InvalidKeyType { expected: "M".to_string(), found: "V".to_string() });
199		"module id has wrong prefix")]
200    fn test_parse(value: &str, prefix: char, is_seed: bool) -> Result<String, ParseError> {
201        parse(value, prefix, is_seed)
202    }
203
204    #[test]
205    fn seed_default() {
206        assert_eq!(ClusterSeed::default(), Seed::<'C'>(String::new()));
207        assert_eq!(Seed::<'M'>::default(), Seed::<'M'>(String::new()));
208    }
209
210    #[test]
211    fn module_id_round_trip() {
212        let a = "M0000000000000000000000000000000000000000000000000000000";
213        let b = a.parse::<ModuleId>().unwrap();
214        assert_eq!(a.to_string(), b.to_string());
215    }
216
217    #[test]
218    fn service_id_round_trip() {
219        let a = "V0000000000000000000000000000000000000000000000000000000";
220        let b = a.parse::<ServiceId>().unwrap();
221        assert_eq!(a.to_string(), b.to_string());
222    }
223
224    #[test]
225    fn server_id_round_trip() {
226        let a = "N0000000000000000000000000000000000000000000000000000000";
227        let b = a.parse::<ServerId>().unwrap();
228        assert_eq!(a.to_string(), b.to_string());
229    }
230
231    #[test]
232    fn cluster_seed_round_trip() {
233        let a = "SC00000000000000000000000000000000000000000000000000000000";
234        let b = a.parse::<ClusterSeed>().unwrap();
235        assert_eq!(a.to_string(), b.to_string());
236    }
237}