Skip to main content

ic_timers/snapshot/
identity.rs

1//! Bounded, deterministically ordered timer identity.
2
3use std::fmt;
4use thiserror::Error;
5
6/// Maximum encoded length of one timer identity component.
7///
8/// The limit is measured in UTF-8 bytes so storage and metrics cardinality do
9/// not depend on Unicode scalar width.
10pub const MAX_TIMER_IDENTITY_COMPONENT_BYTES: usize = 64;
11
12/// The identity component whose validation failed.
13#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub enum TimerIdentityField {
15    /// Scheduling owner, such as a framework or application.
16    Owner,
17    /// Functional area within the owner.
18    Subsystem,
19    /// Stable logical timer name.
20    Name,
21}
22
23impl fmt::Display for TimerIdentityField {
24    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25        formatter.write_str(match self {
26            Self::Owner => "owner",
27            Self::Subsystem => "subsystem",
28            Self::Name => "name",
29        })
30    }
31}
32
33/// Invalid structured timer identity.
34#[non_exhaustive]
35#[derive(Clone, Debug, Eq, Error, PartialEq)]
36pub enum TimerIdentityError {
37    /// One identity component was empty.
38    #[error("invalid timer {field}: identity component must not be empty")]
39    Empty {
40        /// Identity component that failed validation.
41        field: TimerIdentityField,
42    },
43    /// One identity component exceeded the UTF-8 byte bound.
44    #[error(
45        "invalid timer {field}: identity component is {actual_bytes} bytes; maximum is {max_bytes}"
46    )]
47    TooLong {
48        /// Identity component that failed validation.
49        field: TimerIdentityField,
50        /// Encoded length of the rejected value.
51        actual_bytes: usize,
52        /// Maximum supported encoded length.
53        max_bytes: usize,
54    },
55    /// One identity component had leading or trailing whitespace.
56    #[error("invalid timer {field}: identity component has leading or trailing whitespace")]
57    SurroundingWhitespace {
58        /// Identity component that failed validation.
59        field: TimerIdentityField,
60    },
61    /// One identity component contained a control character.
62    #[error(
63        "invalid timer {field}: identity component contains control character {character:?} at byte {byte_index}"
64    )]
65    ControlCharacter {
66        /// Identity component that failed validation.
67        field: TimerIdentityField,
68        /// UTF-8 byte offset of the rejected character.
69        byte_index: usize,
70        /// Rejected control character.
71        character: char,
72    },
73}
74
75/// Stable, low-cardinality identity for one logical canister timer.
76///
77/// Derived ordering is lexicographic by owner, subsystem, then name.
78#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
79pub struct TimerIdentity {
80    owner: Box<str>,
81    subsystem: Box<str>,
82    name: Box<str>,
83}
84
85impl TimerIdentity {
86    /// Validate all three identity components.
87    pub fn try_new(
88        owner: impl AsRef<str>,
89        subsystem: impl AsRef<str>,
90        name: impl AsRef<str>,
91    ) -> Result<Self, TimerIdentityError> {
92        Ok(Self {
93            owner: validate_component(TimerIdentityField::Owner, owner)?,
94            subsystem: validate_component(TimerIdentityField::Subsystem, subsystem)?,
95            name: validate_component(TimerIdentityField::Name, name)?,
96        })
97    }
98
99    /// Return the scheduling owner.
100    #[must_use]
101    pub fn owner(&self) -> &str {
102        &self.owner
103    }
104
105    /// Return the functional subsystem.
106    #[must_use]
107    pub fn subsystem(&self) -> &str {
108        &self.subsystem
109    }
110
111    /// Return the stable logical timer name.
112    #[must_use]
113    pub fn name(&self) -> &str {
114        &self.name
115    }
116}
117
118fn validate_component(
119    field: TimerIdentityField,
120    value: impl AsRef<str>,
121) -> Result<Box<str>, TimerIdentityError> {
122    let value = value.as_ref();
123    if value.is_empty() {
124        return Err(TimerIdentityError::Empty { field });
125    }
126    if value.len() > MAX_TIMER_IDENTITY_COMPONENT_BYTES {
127        return Err(TimerIdentityError::TooLong {
128            field,
129            actual_bytes: value.len(),
130            max_bytes: MAX_TIMER_IDENTITY_COMPONENT_BYTES,
131        });
132    }
133    if value.trim() != value {
134        return Err(TimerIdentityError::SurroundingWhitespace { field });
135    }
136    if let Some((byte_index, character)) = value.char_indices().find(|(_, ch)| ch.is_control()) {
137        return Err(TimerIdentityError::ControlCharacter {
138            field,
139            byte_index,
140            character,
141        });
142    }
143
144    Ok(value.into())
145}