#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
pub const OCI_TITLE: &str = "org.opencontainers.image.title";
pub const OCI_DESCRIPTION: &str = "org.opencontainers.image.description";
pub const OCI_VERSION: &str = "org.opencontainers.image.version";
pub const OCI_SOURCE: &str = "org.opencontainers.image.source";
pub const OCI_REVISION: &str = "org.opencontainers.image.revision";
pub const OCI_LICENSES: &str = "org.opencontainers.image.licenses";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DockerLabelError {
EmptyKey,
InvalidKey,
InvalidValue,
}
impl fmt::Display for DockerLabelError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyKey => formatter.write_str("Docker label key cannot be empty"),
Self::InvalidKey => formatter.write_str("invalid Docker label key"),
Self::InvalidValue => formatter.write_str("invalid Docker label value"),
}
}
}
impl Error for DockerLabelError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DockerLabelKey(String);
impl DockerLabelKey {
pub fn new(value: impl AsRef<str>) -> Result<Self, DockerLabelError> {
let trimmed = value.as_ref().trim();
validate_key(trimmed)?;
Ok(Self(trimmed.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for DockerLabelKey {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for DockerLabelKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for DockerLabelKey {
type Err = DockerLabelError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DockerLabel {
key: DockerLabelKey,
value: String,
}
impl DockerLabel {
pub fn new(key: DockerLabelKey, value: impl AsRef<str>) -> Result<Self, DockerLabelError> {
let value = value.as_ref();
if value.contains('\0') {
return Err(DockerLabelError::InvalidValue);
}
Ok(Self {
key,
value: value.to_string(),
})
}
pub fn oci_title(value: impl AsRef<str>) -> Result<Self, DockerLabelError> {
Self::new(DockerLabelKey::new(OCI_TITLE)?, value)
}
#[must_use]
pub const fn key(&self) -> &DockerLabelKey {
&self.key
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
}
impl fmt::Display for DockerLabel {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}={}", self.key, self.value)
}
}
fn validate_key(value: &str) -> Result<(), DockerLabelError> {
if value.is_empty() {
return Err(DockerLabelError::EmptyKey);
}
if value.starts_with(['.', '/', '-'])
|| value.ends_with(['.', '/', '-'])
|| value.chars().any(char::is_whitespace)
|| !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_' | b'/'))
{
Err(DockerLabelError::InvalidKey)
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{DockerLabel, DockerLabelError, DockerLabelKey, OCI_TITLE};
#[test]
fn validates_and_renders_labels() -> Result<(), Box<dyn std::error::Error>> {
let label = DockerLabel::new(DockerLabelKey::new(OCI_TITLE)?, "RustUse app")?;
assert_eq!(
label.to_string(),
"org.opencontainers.image.title=RustUse app"
);
assert_eq!(
DockerLabelKey::new("bad key"),
Err(DockerLabelError::InvalidKey)
);
assert_eq!(DockerLabel::oci_title("RustUse")?.key().as_str(), OCI_TITLE);
Ok(())
}
}