Skip to main content

cloud_sdk/rate_limit/
extension.rs

1use core::fmt;
2
3use cloud_sdk_sanitization::{sanitize_bytes, sanitize_value};
4
5/// Maximum exact-byte quota extension name length.
6pub const MAX_QUOTA_EXTENSION_NAME_BYTES: usize = 32;
7/// Maximum exact-byte quota extension value length.
8pub const MAX_QUOTA_EXTENSION_VALUE_BYTES: usize = 64;
9
10/// Bounded informational quota extension.
11///
12/// Values are retained exactly but redacted from `Debug` because provider
13/// partition keys and policy parameters can disclose account structure.
14/// They are metadata, not secret storage. Drop performs best-effort cleanup of
15/// the final live owner, but ordinary Rust moves can leave stale inline bytes.
16/// Credentials and other secrets require stable cleanup-owning storage.
17#[derive(Clone, Eq, PartialEq)]
18pub struct QuotaExtension {
19    name: [u8; MAX_QUOTA_EXTENSION_NAME_BYTES],
20    name_len: u8,
21    value: [u8; MAX_QUOTA_EXTENSION_VALUE_BYTES],
22    value_len: u8,
23}
24
25impl QuotaExtension {
26    /// Copies informational visible-ASCII metadata into fixed-capacity storage.
27    pub fn new(name: &[u8], value: &[u8]) -> Result<Self, QuotaExtensionError> {
28        validate(name, MAX_QUOTA_EXTENSION_NAME_BYTES, true)?;
29        validate(value, MAX_QUOTA_EXTENSION_VALUE_BYTES, false)?;
30        let mut result = Self {
31            name: [0; MAX_QUOTA_EXTENSION_NAME_BYTES],
32            name_len: u8::try_from(name.len()).map_err(|_| QuotaExtensionError::NameTooLong)?,
33            value: [0; MAX_QUOTA_EXTENSION_VALUE_BYTES],
34            value_len: u8::try_from(value.len()).map_err(|_| QuotaExtensionError::ValueTooLong)?,
35        };
36        let name_target = result
37            .name
38            .get_mut(..name.len())
39            .ok_or(QuotaExtensionError::NameTooLong)?;
40        name_target.copy_from_slice(name);
41        let value_target = result
42            .value
43            .get_mut(..value.len())
44            .ok_or(QuotaExtensionError::ValueTooLong)?;
45        value_target.copy_from_slice(value);
46        Ok(result)
47    }
48
49    /// Returns the exact extension name bytes.
50    #[must_use]
51    pub fn name(&self) -> &[u8] {
52        self.name
53            .get(..usize::from(self.name_len))
54            .unwrap_or_default()
55    }
56
57    /// Returns the exact extension value bytes.
58    #[must_use]
59    pub fn value(&self) -> &[u8] {
60        self.value
61            .get(..usize::from(self.value_len))
62            .unwrap_or_default()
63    }
64
65    fn clear(&mut self) {
66        sanitize_bytes(&mut self.name);
67        sanitize_bytes(&mut self.value);
68        sanitize_value(&mut self.name_len);
69        sanitize_value(&mut self.value_len);
70    }
71}
72
73impl Drop for QuotaExtension {
74    fn drop(&mut self) {
75        self.clear();
76    }
77}
78
79impl fmt::Debug for QuotaExtension {
80    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81        formatter
82            .debug_struct("QuotaExtension")
83            .field("name", &self.name())
84            .field("value", &"[redacted]")
85            .finish()
86    }
87}
88
89fn validate(value: &[u8], maximum: usize, name: bool) -> Result<(), QuotaExtensionError> {
90    if value.is_empty() {
91        return Err(if name {
92            QuotaExtensionError::NameEmpty
93        } else {
94            QuotaExtensionError::ValueEmpty
95        });
96    }
97    if value.len() > maximum {
98        return Err(if name {
99            QuotaExtensionError::NameTooLong
100        } else {
101            QuotaExtensionError::ValueTooLong
102        });
103    }
104    if !value.iter().all(|byte| byte.is_ascii_graphic()) {
105        return Err(QuotaExtensionError::InvalidByte);
106    }
107    Ok(())
108}
109
110/// Invalid informational quota extension.
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112pub enum QuotaExtensionError {
113    /// The extension name was empty.
114    NameEmpty,
115    /// The extension name exceeded its bound.
116    NameTooLong,
117    /// The extension value was empty.
118    ValueEmpty,
119    /// The extension value exceeded its bound.
120    ValueTooLong,
121    /// A name or value contained whitespace, a control, or non-ASCII data.
122    InvalidByte,
123}
124
125impl_static_error!(QuotaExtensionError,
126    Self::NameEmpty => "quota extension name is empty",
127    Self::NameTooLong => "quota extension name exceeds its length limit",
128    Self::ValueEmpty => "quota extension value is empty",
129    Self::ValueTooLong => "quota extension value exceeds its length limit",
130    Self::InvalidByte => "quota extension contains an invalid byte",
131);
132
133#[cfg(test)]
134mod tests {
135    use super::QuotaExtension;
136
137    #[test]
138    fn explicit_cleanup_clears_complete_name_value_and_lengths() {
139        let extension = QuotaExtension::new(b"partition", b"account-42");
140        assert!(extension.is_ok());
141        let Ok(mut extension) = extension else {
142            return;
143        };
144
145        extension.clear();
146
147        assert!(extension.name.iter().all(|byte| *byte == 0));
148        assert!(extension.value.iter().all(|byte| *byte == 0));
149        assert_eq!(extension.name_len, 0);
150        assert_eq!(extension.value_len, 0);
151    }
152}