#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DockerVolumeError {
Empty,
EmptyTarget,
InvalidAccess,
}
impl fmt::Display for DockerVolumeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("Docker volume mount cannot be empty"),
Self::EmptyTarget => formatter.write_str("Docker volume target cannot be empty"),
Self::InvalidAccess => formatter.write_str("Docker volume access must be ro or rw"),
}
}
}
impl Error for DockerVolumeError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum MountAccess {
ReadWrite,
ReadOnly,
}
impl MountAccess {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::ReadWrite => "rw",
Self::ReadOnly => "ro",
}
}
}
impl fmt::Display for MountAccess {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for MountAccess {
type Err = DockerVolumeError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"rw" => Ok(Self::ReadWrite),
"ro" => Ok(Self::ReadOnly),
_ => Err(DockerVolumeError::InvalidAccess),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum VolumeKind {
Anonymous,
Named,
Bind,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DockerVolumeMount {
source: Option<String>,
target: String,
kind: VolumeKind,
access: MountAccess,
}
impl DockerVolumeMount {
pub fn new(
source: Option<String>,
target: impl AsRef<str>,
access: MountAccess,
) -> Result<Self, DockerVolumeError> {
let target = target.as_ref().trim();
if target.is_empty() {
return Err(DockerVolumeError::EmptyTarget);
}
let kind = source
.as_deref()
.map_or(VolumeKind::Anonymous, classify_source);
Ok(Self {
source,
target: target.to_string(),
kind,
access,
})
}
#[must_use]
pub fn source(&self) -> Option<&str> {
self.source.as_deref()
}
#[must_use]
pub fn target(&self) -> &str {
&self.target
}
#[must_use]
pub const fn kind(&self) -> VolumeKind {
self.kind
}
#[must_use]
pub const fn access(&self) -> MountAccess {
self.access
}
}
impl fmt::Display for DockerVolumeMount {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(source) = &self.source {
write!(formatter, "{source}:")?;
}
write!(formatter, "{}", self.target)?;
if self.access == MountAccess::ReadOnly {
write!(formatter, ":{}", self.access)?;
}
Ok(())
}
}
impl FromStr for DockerVolumeMount {
type Err = DockerVolumeError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
parse_mount(value)
}
}
impl TryFrom<&str> for DockerVolumeMount {
type Error = DockerVolumeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
parse_mount(value)
}
}
fn parse_mount(value: &str) -> Result<DockerVolumeMount, DockerVolumeError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(DockerVolumeError::Empty);
}
let (body, access) = match trimmed.rsplit_once(':') {
Some((body, mode)) if matches!(mode, "ro" | "rw") => (body, mode.parse()?),
_ => (trimmed, MountAccess::ReadWrite),
};
let (source, target) = match body.rsplit_once(':') {
Some((source, target)) => (Some(source.to_string()), target),
None => (None, body),
};
DockerVolumeMount::new(source, target, access)
}
fn classify_source(source: &str) -> VolumeKind {
let bytes = source.as_bytes();
if source.starts_with(['/', '.', '~'])
|| source.contains('\\')
|| bytes.get(1).is_some_and(|byte| *byte == b':')
{
VolumeKind::Bind
} else {
VolumeKind::Named
}
}
#[cfg(test)]
mod tests {
use super::{DockerVolumeMount, MountAccess, VolumeKind};
#[test]
fn parses_volume_mounts() -> Result<(), Box<dyn std::error::Error>> {
let named: DockerVolumeMount = "cache:/var/cache:ro".parse()?;
let bind: DockerVolumeMount = "C:\\data:/data".parse()?;
let anonymous: DockerVolumeMount = "/var/lib/app".parse()?;
assert_eq!(named.kind(), VolumeKind::Named);
assert_eq!(named.access(), MountAccess::ReadOnly);
assert_eq!(bind.kind(), VolumeKind::Bind);
assert_eq!(anonymous.kind(), VolumeKind::Anonymous);
assert_eq!(named.to_string(), "cache:/var/cache:ro");
Ok(())
}
}