appcore_contracts/
identifiers.rs1use crate::{ContractError, ContractResult};
14use serde::{Deserialize, Deserializer, Serialize};
15use std::fmt::{Display, Formatter};
16use std::str::FromStr;
17
18pub(crate) fn validate_text(
19 field: &'static str,
20 value: &str,
21 max_bytes: usize,
22) -> ContractResult<()> {
23 if value.trim().is_empty() {
24 return Err(ContractError::Empty { field });
25 }
26 if value.len() > max_bytes {
27 return Err(ContractError::TooLong { field, max_bytes });
28 }
29 if value.chars().any(char::is_control) {
30 return Err(ContractError::InvalidValue {
31 field,
32 reason: "control characters are not allowed",
33 });
34 }
35 Ok(())
36}
37
38pub(crate) fn validate_identifier(field: &'static str, value: &str) -> ContractResult<()> {
39 validate_text(field, value, 128)?;
40 let mut chars = value.chars();
41 let Some(first) = chars.next() else {
42 return Err(ContractError::InvalidIdentifier { field });
43 };
44 if !first.is_ascii_alphanumeric()
45 || !value
46 .chars()
47 .all(|character| character.is_ascii_alphanumeric() || "-_.:+".contains(character))
48 || !value
49 .chars()
50 .last()
51 .is_some_and(|character| character.is_ascii_alphanumeric())
52 {
53 return Err(ContractError::InvalidIdentifier { field });
54 }
55 Ok(())
56}
57
58pub(crate) fn is_sensitive_key(value: &str) -> bool {
59 let normalized = value.to_ascii_lowercase();
60 [
61 "secret",
62 "password",
63 "passwd",
64 "token",
65 "private_key",
66 "credential",
67 ]
68 .iter()
69 .any(|fragment| normalized.contains(fragment))
70}
71
72pub(crate) fn looks_like_local_path(value: &str) -> bool {
73 value.starts_with('/')
74 || value.starts_with("./")
75 || value.starts_with("../")
76 || value.as_bytes().get(1) == Some(&b':')
77}
78
79pub(crate) fn looks_like_url(value: &str) -> bool {
80 value.contains("://")
81}
82
83macro_rules! define_identifier {
84 ($name:ident, $field:literal, $docs:literal) => {
85 #[doc = $docs]
86 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
87 #[serde(transparent)]
88 pub struct $name(String);
89
90 impl $name {
91 pub fn new(value: impl Into<String>) -> ContractResult<Self> {
93 let value = value.into();
94 validate_identifier($field, &value)?;
95 Ok(Self(value))
96 }
97
98 pub fn as_str(&self) -> &str {
100 &self.0
101 }
102
103 pub fn into_inner(self) -> String {
105 self.0
106 }
107 }
108
109 impl Display for $name {
110 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
111 formatter.write_str(&self.0)
112 }
113 }
114
115 impl FromStr for $name {
116 type Err = ContractError;
117
118 fn from_str(value: &str) -> Result<Self, Self::Err> {
119 Self::new(value)
120 }
121 }
122
123 impl<'de> Deserialize<'de> for $name {
124 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
125 where
126 D: Deserializer<'de>,
127 {
128 let value = String::deserialize(deserializer)?;
129 Self::new(value).map_err(serde::de::Error::custom)
130 }
131 }
132 };
133}
134
135define_identifier!(
136 ApplicationId,
137 "application_id",
138 "Stable identity of an application."
139);
140define_identifier!(
141 ServiceId,
142 "service_id",
143 "Identity of an independently coordinated service."
144);
145define_identifier!(
146 CapabilityId,
147 "capability_id",
148 "Generic capability name resolved by the runtime."
149);
150define_identifier!(
151 ProviderId,
152 "provider_id",
153 "Identity of a deployment provider or adapter."
154);
155define_identifier!(
156 NodeId,
157 "node_id",
158 "Identity of the physical or virtual runtime node."
159);
160define_identifier!(
161 CoreId,
162 "core_id",
163 "Identity of one executable core hosted on a node."
164);
165define_identifier!(
166 InstallationId,
167 "installation_id",
168 "Identity of one application installation."
169);
170define_identifier!(ModuleId, "module_id", "Identity of an application module.");
171define_identifier!(JobId, "job_id", "Identity of a provider-owned runtime job.");
172define_identifier!(
173 FeatureId,
174 "feature_id",
175 "Identity of a declared runtime or application feature."
176);
177define_identifier!(
178 BuildId,
179 "build_id",
180 "Identity of an immutable runtime or application build."
181);
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn identifiers_reject_paths_and_whitespace() {
189 assert!(ApplicationId::new("app.example").is_ok());
190 assert!(ApplicationId::new("../app").is_err());
191 assert!(ApplicationId::new("app example").is_err());
192 }
193}