use_oci_annotation/
lib.rs1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7pub const OCI_TITLE: &str = "org.opencontainers.image.title";
9pub const OCI_DESCRIPTION: &str = "org.opencontainers.image.description";
11pub const OCI_SOURCE: &str = "org.opencontainers.image.source";
13pub const OCI_REVISION: &str = "org.opencontainers.image.revision";
15pub const OCI_VERSION: &str = "org.opencontainers.image.version";
17pub const OCI_CREATED: &str = "org.opencontainers.image.created";
19pub const OCI_AUTHORS: &str = "org.opencontainers.image.authors";
21pub const OCI_LICENSES: &str = "org.opencontainers.image.licenses";
23pub const OCI_DOCUMENTATION: &str = "org.opencontainers.image.documentation";
25pub const OCI_URL: &str = "org.opencontainers.image.url";
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub enum AnnotationError {
31 EmptyKey,
32 InvalidKey,
33 InvalidValue,
34}
35
36impl fmt::Display for AnnotationError {
37 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 Self::EmptyKey => formatter.write_str("OCI annotation key cannot be empty"),
40 Self::InvalidKey => formatter.write_str("invalid OCI annotation key"),
41 Self::InvalidValue => formatter.write_str("invalid OCI annotation value"),
42 }
43 }
44}
45
46impl Error for AnnotationError {}
47
48#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
50pub struct AnnotationKey(String);
51
52impl AnnotationKey {
53 pub fn new(value: impl AsRef<str>) -> Result<Self, AnnotationError> {
55 let trimmed = value.as_ref().trim();
56 validate_key(trimmed)?;
57 Ok(Self(trimmed.to_string()))
58 }
59
60 #[must_use]
62 pub fn as_str(&self) -> &str {
63 &self.0
64 }
65}
66
67impl AsRef<str> for AnnotationKey {
68 fn as_ref(&self) -> &str {
69 self.as_str()
70 }
71}
72
73impl fmt::Display for AnnotationKey {
74 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75 formatter.write_str(self.as_str())
76 }
77}
78
79impl FromStr for AnnotationKey {
80 type Err = AnnotationError;
81
82 fn from_str(value: &str) -> Result<Self, Self::Err> {
83 Self::new(value)
84 }
85}
86
87impl TryFrom<&str> for AnnotationKey {
88 type Error = AnnotationError;
89
90 fn try_from(value: &str) -> Result<Self, Self::Error> {
91 Self::new(value)
92 }
93}
94
95#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
97pub struct AnnotationValue(String);
98
99impl AnnotationValue {
100 pub fn new(value: impl AsRef<str>) -> Result<Self, AnnotationError> {
102 let value = value.as_ref();
103 if value.contains('\0') {
104 return Err(AnnotationError::InvalidValue);
105 }
106 Ok(Self(value.to_string()))
107 }
108
109 #[must_use]
111 pub fn as_str(&self) -> &str {
112 &self.0
113 }
114}
115
116impl AsRef<str> for AnnotationValue {
117 fn as_ref(&self) -> &str {
118 self.as_str()
119 }
120}
121
122impl fmt::Display for AnnotationValue {
123 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
124 formatter.write_str(self.as_str())
125 }
126}
127
128#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130pub struct Annotation {
131 key: AnnotationKey,
132 value: AnnotationValue,
133}
134
135impl Annotation {
136 pub fn new(key: impl AsRef<str>, value: impl AsRef<str>) -> Result<Self, AnnotationError> {
138 Ok(Self {
139 key: AnnotationKey::new(key)?,
140 value: AnnotationValue::new(value)?,
141 })
142 }
143
144 pub fn title(value: impl AsRef<str>) -> Result<Self, AnnotationError> {
146 Self::new(OCI_TITLE, value)
147 }
148
149 pub fn description(value: impl AsRef<str>) -> Result<Self, AnnotationError> {
151 Self::new(OCI_DESCRIPTION, value)
152 }
153
154 pub fn source(value: impl AsRef<str>) -> Result<Self, AnnotationError> {
156 Self::new(OCI_SOURCE, value)
157 }
158
159 #[must_use]
161 pub const fn key(&self) -> &AnnotationKey {
162 &self.key
163 }
164
165 #[must_use]
167 pub const fn value(&self) -> &AnnotationValue {
168 &self.value
169 }
170}
171
172impl fmt::Display for Annotation {
173 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
174 write!(formatter, "{}={}", self.key, self.value)
175 }
176}
177
178fn validate_key(value: &str) -> Result<(), AnnotationError> {
179 if value.is_empty() {
180 return Err(AnnotationError::EmptyKey);
181 }
182 if value.starts_with(['.', '/', '-'])
183 || value.ends_with(['.', '/', '-'])
184 || value.chars().any(char::is_whitespace)
185 || !value
186 .bytes()
187 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_' | b'/'))
188 {
189 Err(AnnotationError::InvalidKey)
190 } else {
191 Ok(())
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::{Annotation, AnnotationError, AnnotationKey, OCI_TITLE};
198
199 #[test]
200 fn validates_and_renders_annotations() -> Result<(), Box<dyn std::error::Error>> {
201 let annotation = Annotation::title("RustUse OCI")?;
202
203 assert_eq!(annotation.key().as_str(), OCI_TITLE);
204 assert_eq!(
205 annotation.to_string(),
206 "org.opencontainers.image.title=RustUse OCI"
207 );
208 assert_eq!(
209 AnnotationKey::new("bad key"),
210 Err(AnnotationError::InvalidKey)
211 );
212 assert_eq!(
213 Annotation::new("example.key", "bad\0value"),
214 Err(AnnotationError::InvalidValue)
215 );
216 Ok(())
217 }
218}