component_id_index/
instance_id.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4use hex::FromHex;
5use std::fmt::Display;
6use std::str::FromStr;
7use thiserror::Error;
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11
12/// The length of an instance ID, in bytes.
13/// An instance ID is 256 bits, normally encoded as a 64-character hex string.
14pub const INSTANCE_ID_LEN: usize = 32;
15
16#[derive(Clone, Debug, Eq, Hash, PartialEq)]
17pub struct InstanceId([u8; INSTANCE_ID_LEN]);
18
19impl InstanceId {
20    /// Returns a random instance ID.
21    pub fn new_random(rng: &mut impl rand::Rng) -> Self {
22        let mut bytes: [u8; INSTANCE_ID_LEN] = [0; INSTANCE_ID_LEN];
23        rng.fill_bytes(&mut bytes);
24        InstanceId(bytes)
25    }
26
27    pub fn as_bytes(&self) -> &[u8] {
28        &self.0
29    }
30}
31
32impl Display for InstanceId {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
34        for byte in self.0 {
35            write!(f, "{:02x}", byte)?;
36        }
37        Ok(())
38    }
39}
40
41#[cfg_attr(feature = "serde", derive(Deserialize, Serialize), serde(rename_all = "snake_case"))]
42#[derive(Error, Clone, Debug, PartialEq)]
43pub enum InstanceIdError {
44    #[error("invalid length; must be 64 characters")]
45    InvalidLength,
46    #[error("string contains invalid hex character; must be [0-9a-f]")]
47    InvalidHexCharacter,
48}
49
50impl FromStr for InstanceId {
51    type Err = InstanceIdError;
52
53    fn from_str(s: &str) -> Result<Self, Self::Err> {
54        // Must have 64 characters.
55        // 256 bits in base16 = 64 chars (1 char to represent 4 bits)
56        if s.len() != 64 {
57            return Err(InstanceIdError::InvalidLength);
58        }
59        // Must be a lower-cased hex string.
60        if !s.chars().all(|ch| (ch.is_numeric() || ch.is_lowercase()) && ch.is_digit(16)) {
61            return Err(InstanceIdError::InvalidHexCharacter);
62        }
63        // The following unwrap is safe because the validation above covers all FromHexError cases.
64        let bytes = <[u8; INSTANCE_ID_LEN]>::from_hex(&s).unwrap();
65        Ok(InstanceId(bytes))
66    }
67}
68
69#[cfg(feature = "serde")]
70impl Serialize for InstanceId {
71    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
72    where
73        S: Serializer,
74    {
75        serializer.collect_str(self)
76    }
77}
78
79#[cfg(feature = "serde")]
80impl<'de> Deserialize<'de> for InstanceId {
81    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
82    where
83        D: Deserializer<'de>,
84    {
85        String::deserialize(deserializer)?.parse().map_err(|err| {
86            let instance_id = InstanceId::new_random(&mut rand::thread_rng());
87            serde::de::Error::custom(format!(
88                "Invalid instance ID: {}\n\nHere is a valid, randomly generated ID: {}\n",
89                err, instance_id
90            ))
91        })
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use proptest::prelude::*;
99    use rand::SeedableRng as _;
100    use test_case::test_case;
101
102    #[test]
103    fn to_string() {
104        let id_str = "8c90d44863ff67586cf6961081feba4f760decab8bbbee376a3bfbc77b351280";
105        let id = id_str.parse::<InstanceId>().unwrap();
106        assert_eq!(id.to_string(), id_str);
107    }
108
109    proptest! {
110        #[test]
111        fn parse(id in "[a-f0-9]{64}") {
112            prop_assert!(id.parse::<InstanceId>().is_ok());
113        }
114    }
115
116    #[test_case("8c90d44863ff67586cf6961081feba4f760decab8bbbee376a3bfbc77b351280b351280"; "too long")]
117    #[test_case("8c90d44863ff67586cf6961081feba4f760decab8bbbee376a"; "too short")]
118    #[test_case("8C90D44863FF67586CF6961081FEBA4F760DECAB8BBBEE376A3BFBC77B351280"; "upper case chars are invalid")]
119    #[test_case("8;90d44863ff67586cf6961081feba4f760decab8bbbee376a3bfbc77b351280"; "hex chars only")]
120    fn parse_invalid(id: &str) {
121        assert!(id.parse::<InstanceId>().is_err());
122    }
123
124    #[test]
125    fn new_random_is_unique() {
126        let seed = rand::thread_rng().next_u64();
127        println!("using seed {}", seed);
128        let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
129        let mut prev_id = InstanceId::new_random(&mut rng);
130        for _i in 0..40 {
131            let id = InstanceId::new_random(&mut rng);
132            assert!(prev_id != id);
133            prev_id = id;
134        }
135    }
136
137    #[test]
138    fn parse_new_random() {
139        let seed = rand::thread_rng().next_u64();
140        println!("using seed {}", seed);
141        let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
142        for _i in 0..40 {
143            let id_str = InstanceId::new_random(&mut rng).to_string();
144            assert!(id_str.parse::<InstanceId>().is_ok());
145        }
146    }
147}