1#![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_VERSION: &str = "org.opencontainers.image.version";
13pub const OCI_SOURCE: &str = "org.opencontainers.image.source";
15pub const OCI_REVISION: &str = "org.opencontainers.image.revision";
17pub const OCI_LICENSES: &str = "org.opencontainers.image.licenses";
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum DockerLabelError {
23 EmptyKey,
25 InvalidKey,
27 InvalidValue,
29}
30
31impl fmt::Display for DockerLabelError {
32 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 Self::EmptyKey => formatter.write_str("Docker label key cannot be empty"),
35 Self::InvalidKey => formatter.write_str("invalid Docker label key"),
36 Self::InvalidValue => formatter.write_str("invalid Docker label value"),
37 }
38 }
39}
40
41impl Error for DockerLabelError {}
42
43#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
45pub struct DockerLabelKey(String);
46
47impl DockerLabelKey {
48 pub fn new(value: impl AsRef<str>) -> Result<Self, DockerLabelError> {
50 let trimmed = value.as_ref().trim();
51 validate_key(trimmed)?;
52 Ok(Self(trimmed.to_string()))
53 }
54
55 #[must_use]
57 pub fn as_str(&self) -> &str {
58 &self.0
59 }
60}
61
62impl AsRef<str> for DockerLabelKey {
63 fn as_ref(&self) -> &str {
64 self.as_str()
65 }
66}
67
68impl fmt::Display for DockerLabelKey {
69 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70 formatter.write_str(self.as_str())
71 }
72}
73
74impl FromStr for DockerLabelKey {
75 type Err = DockerLabelError;
76
77 fn from_str(value: &str) -> Result<Self, Self::Err> {
78 Self::new(value)
79 }
80}
81
82#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
84pub struct DockerLabel {
85 key: DockerLabelKey,
86 value: String,
87}
88
89impl DockerLabel {
90 pub fn new(key: DockerLabelKey, value: impl AsRef<str>) -> Result<Self, DockerLabelError> {
92 let value = value.as_ref();
93 if value.contains('\0') {
94 return Err(DockerLabelError::InvalidValue);
95 }
96 Ok(Self {
97 key,
98 value: value.to_string(),
99 })
100 }
101
102 pub fn oci_title(value: impl AsRef<str>) -> Result<Self, DockerLabelError> {
104 Self::new(DockerLabelKey::new(OCI_TITLE)?, value)
105 }
106
107 #[must_use]
109 pub const fn key(&self) -> &DockerLabelKey {
110 &self.key
111 }
112
113 #[must_use]
115 pub fn value(&self) -> &str {
116 &self.value
117 }
118}
119
120impl fmt::Display for DockerLabel {
121 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
122 write!(formatter, "{}={}", self.key, self.value)
123 }
124}
125
126fn validate_key(value: &str) -> Result<(), DockerLabelError> {
127 if value.is_empty() {
128 return Err(DockerLabelError::EmptyKey);
129 }
130 if value.starts_with(['.', '/', '-'])
131 || value.ends_with(['.', '/', '-'])
132 || value.chars().any(char::is_whitespace)
133 || !value
134 .bytes()
135 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_' | b'/'))
136 {
137 Err(DockerLabelError::InvalidKey)
138 } else {
139 Ok(())
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::{DockerLabel, DockerLabelError, DockerLabelKey, OCI_TITLE};
146
147 #[test]
148 fn validates_and_renders_labels() -> Result<(), Box<dyn std::error::Error>> {
149 let label = DockerLabel::new(DockerLabelKey::new(OCI_TITLE)?, "RustUse app")?;
150
151 assert_eq!(
152 label.to_string(),
153 "org.opencontainers.image.title=RustUse app"
154 );
155 assert_eq!(
156 DockerLabelKey::new("bad key"),
157 Err(DockerLabelError::InvalidKey)
158 );
159 assert_eq!(DockerLabel::oci_title("RustUse")?.key().as_str(), OCI_TITLE);
160 Ok(())
161 }
162}