use std::{error::Error, fmt};
use crate::{
diagnostic::Diagnostic,
model::{
BuildKey, ContainerKey, EntryKind, ImageKey, NetworkKey, PodKey, QuadletDocument, QuadletParseResult,
QuadletUnitType, SectionKind, TypedModelError, VolumeKey,
},
source::SourceId,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntryValue(String);
impl EntryValue {
pub fn new(value: impl Into<String>) -> Result<Self, RenderError> {
let value = value.into();
if value.bytes().any(|byte| matches!(byte, 0 | b'\n' | b'\r')) {
return Err(RenderError::InvalidValue);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PidsLimit(String);
impl PidsLimit {
#[must_use]
pub fn unlimited() -> Self {
Self("-1".to_owned())
}
pub fn finite(limit: impl Into<String>) -> Result<Self, PidsLimitError> {
let limit = limit.into();
if limit.is_empty() {
return Err(PidsLimitError::Empty);
}
if !limit.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(PidsLimitError::NonDecimal);
}
if !limit.bytes().any(|byte| byte != b'0') {
return Err(PidsLimitError::Zero);
}
Ok(Self(limit))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<PidsLimit> for EntryValue {
fn from(limit: PidsLimit) -> Self {
Self(limit.0)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PidsLimitError {
Empty,
NonDecimal,
Zero,
}
impl fmt::Display for PidsLimitError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("a finite process-ID limit must not be empty"),
Self::NonDecimal => formatter.write_str("a finite process-ID limit must contain only ASCII decimal digits"),
Self::Zero => formatter.write_str("a finite process-ID limit must be positive"),
}
}
}
impl Error for PidsLimitError {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ShmSize(String);
impl ShmSize {
pub fn new(size: impl Into<String>) -> Result<Self, ShmSizeError> {
let size = size.into();
if size.is_empty() {
return Err(ShmSizeError::Empty);
}
let amount = match size.as_bytes().last() {
Some(b'b' | b'k' | b'm' | b'g') => &size[..size.len() - 1],
_ => size.as_str(),
};
if amount.is_empty() || !amount.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(ShmSizeError::InvalidFormat);
}
Ok(Self(size))
}
#[must_use]
pub fn unlimited() -> Self {
Self("0".to_owned())
}
#[must_use]
pub fn is_unlimited(&self) -> bool {
let amount = match self.0.as_bytes().last() {
Some(b'b' | b'k' | b'm' | b'g') => &self.0[..self.0.len() - 1],
_ => self.0.as_str(),
};
amount.bytes().all(|byte| byte == b'0')
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<ShmSize> for EntryValue {
fn from(size: ShmSize) -> Self {
Self(size.0)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ShmSizeError {
Empty,
InvalidFormat,
}
impl fmt::Display for ShmSizeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("a shared-memory size must not be empty"),
Self::InvalidFormat => formatter
.write_str("a shared-memory size must be an ASCII decimal amount with optional unit b, k, m, or g"),
}
}
}
impl Error for ShmSizeError {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Memory(String);
impl Memory {
pub fn new(memory: impl Into<String>) -> Result<Self, MemoryError> {
let memory = memory.into();
if memory.is_empty() {
return Err(MemoryError::Empty);
}
let amount = match memory.as_bytes().last() {
Some(b'b' | b'k' | b'm' | b'g') => &memory[..memory.len() - 1],
_ => memory.as_str(),
};
if amount.is_empty() || !amount.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(MemoryError::InvalidFormat);
}
if !amount.bytes().any(|byte| byte != b'0') {
return Err(MemoryError::Zero);
}
Ok(Self(memory))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<Memory> for EntryValue {
fn from(memory: Memory) -> Self {
Self(memory.0)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum MemoryError {
Empty,
InvalidFormat,
Zero,
}
impl fmt::Display for MemoryError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("a memory limit must not be empty"),
Self::InvalidFormat => {
formatter.write_str("a memory limit must be an ASCII decimal amount with optional unit b, k, m, or g")
}
Self::Zero => formatter.write_str("a memory limit must be positive"),
}
}
}
impl Error for MemoryError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SystemdSection {
Unit,
Service,
Install,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SystemdUnitKey {
Requires,
Wants,
After,
}
impl SystemdUnitKey {
const fn name(self) -> &'static str {
match self {
Self::Requires => "Requires",
Self::Wants => "Wants",
Self::After => "After",
}
}
}
impl SystemdSection {
const fn kind(self) -> SectionKind {
match self {
Self::Unit => SectionKind::Unit,
Self::Service => SectionKind::Service,
Self::Install => SectionKind::Install,
}
}
}
#[derive(Clone, Eq, PartialEq)]
struct GeneratedEntry {
section: SectionKind,
kind: EntryKind,
key: String,
value: EntryValue,
}
impl fmt::Debug for GeneratedEntry {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = formatter.debug_struct("GeneratedEntry");
debug
.field("section", &self.section)
.field("kind", &self.kind)
.field("key", &self.key);
if self.kind.has_sensitive_value() {
debug.field("value", &"<redacted sensitive value>")
} else {
debug.field("value", &self.value)
};
debug.finish()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QuadletDocumentBuilder {
unit_type: QuadletUnitType,
entries: Vec<GeneratedEntry>,
}
impl QuadletDocumentBuilder {
#[must_use]
pub const fn new(unit_type: QuadletUnitType) -> Self {
Self {
unit_type,
entries: Vec::new(),
}
}
#[must_use]
pub const fn unit_type(&self) -> QuadletUnitType {
self.unit_type
}
pub fn push_container(&mut self, key: ContainerKey, value: EntryValue) -> Result<(), RenderError> {
let attempted = container_key_name(key);
if let Some(existing) = match key {
ContainerKey::ReloadCmd => self.entries.iter().find_map(|entry| {
(entry.kind == EntryKind::Container(ContainerKey::ReloadSignal)).then_some("ReloadSignal")
}),
ContainerKey::ReloadSignal => self
.entries
.iter()
.find_map(|entry| (entry.kind == EntryKind::Container(ContainerKey::ReloadCmd)).then_some("ReloadCmd")),
_ => None,
} {
return Err(RenderError::ConflictingSingletons {
existing: existing.to_owned(),
attempted: attempted.to_owned(),
});
}
self.push_native(
QuadletUnitType::Container,
SectionKind::Container,
EntryKind::Container(key),
container_key_name(key),
value,
)
}
pub fn push_pod(&mut self, key: PodKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Pod,
SectionKind::Pod,
EntryKind::Pod(key),
pod_key_name(key),
value,
)
}
pub fn push_network(&mut self, key: NetworkKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Network,
SectionKind::Network,
EntryKind::Network(key),
network_key_name(key),
value,
)
}
pub fn push_volume(&mut self, key: VolumeKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Volume,
SectionKind::Volume,
EntryKind::Volume(key),
volume_key_name(key),
value,
)
}
pub fn push_build(&mut self, key: BuildKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Build,
SectionKind::Build,
EntryKind::Build(key),
build_key_name(key),
value,
)
}
pub fn push_image(&mut self, key: ImageKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Image,
SectionKind::Image,
EntryKind::Image(key),
image_key_name(key),
value,
)
}
pub fn push_systemd(
&mut self,
section: SystemdSection,
key: impl Into<String>,
value: EntryValue,
) -> Result<(), RenderError> {
let key = key.into();
if key.is_empty() || !key.bytes().all(|byte| byte.is_ascii_alphanumeric()) {
return Err(RenderError::InvalidKey(key));
}
self.entries.push(GeneratedEntry {
section: section.kind(),
kind: EntryKind::GenericSystemd,
key,
value,
});
Ok(())
}
pub fn push_systemd_unit(&mut self, key: SystemdUnitKey, value: EntryValue) -> Result<(), RenderError> {
self.push_systemd(SystemdSection::Unit, key.name(), value)
}
pub fn build(&self, source_id: SourceId) -> Result<GeneratedQuadletDocument, RenderError> {
let text = self.render_text();
let parsed = QuadletDocument::parse(self.unit_type, source_id, text).map_err(RenderError::TypedModel)?;
if !parsed.is_valid() {
let mut diagnostics = parsed.syntax().diagnostics().to_vec();
diagnostics.extend_from_slice(parsed.model_diagnostics());
return Err(RenderError::InvalidDocument(diagnostics));
}
Ok(GeneratedQuadletDocument { parsed })
}
fn push_native(
&mut self,
required: QuadletUnitType,
section: SectionKind,
kind: EntryKind,
key: &'static str,
value: EntryValue,
) -> Result<(), RenderError> {
if self.unit_type != required {
return Err(RenderError::WrongUnitType {
document: self.unit_type,
entry: required,
});
}
if !kind.is_repeatable() && self.entries.iter().any(|entry| entry.kind == kind) {
return Err(RenderError::DuplicateSingleton(key.to_owned()));
}
self.entries.push(GeneratedEntry {
section,
kind,
key: key.to_owned(),
value,
});
Ok(())
}
fn render_text(&self) -> String {
let native = self.unit_type.native_section();
let sections = [SectionKind::Unit, native, SectionKind::Service, SectionKind::Install];
let mut output = String::new();
let mut wrote_section = false;
for section in sections {
let entries: Vec<_> = self.entries.iter().filter(|entry| entry.section == section).collect();
if entries.is_empty() && section != native {
continue;
}
if wrote_section {
output.push('\n');
}
wrote_section = true;
output.push('[');
output.push_str(section_name(section));
output.push_str("]\n");
for entry in entries {
output.push_str(&entry.key);
output.push('=');
output.push_str(entry.value.as_str());
output.push('\n');
}
}
output
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GeneratedQuadletDocument {
parsed: QuadletParseResult,
}
impl GeneratedQuadletDocument {
#[must_use]
pub fn text(&self) -> &str {
self.parsed.syntax().document().render_preserved()
}
#[must_use]
pub const fn document(&self) -> &QuadletDocument {
self.parsed.document()
}
#[must_use]
pub const fn parse_result(&self) -> &QuadletParseResult {
&self.parsed
}
#[must_use]
pub fn into_parse_result(self) -> QuadletParseResult {
self.parsed
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RenderError {
InvalidValue,
InvalidKey(String),
WrongUnitType {
document: QuadletUnitType,
entry: QuadletUnitType,
},
DuplicateSingleton(String),
ConflictingSingletons {
existing: String,
attempted: String,
},
InvalidDocument(Vec<Diagnostic>),
TypedModel(TypedModelError),
}
impl RenderError {
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
match self {
Self::InvalidDocument(diagnostics) => diagnostics,
_ => &[],
}
}
}
impl fmt::Display for RenderError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidValue => formatter.write_str("generated Quadlet values must fit on one physical line"),
Self::InvalidKey(key) => write!(formatter, "invalid generic systemd key `{key}`"),
Self::WrongUnitType { document, entry } => {
write!(formatter, "cannot add a {entry:?} entry to a {document:?} document")
}
Self::DuplicateSingleton(key) => write!(formatter, "singleton Quadlet key `{key}` is repeated"),
Self::ConflictingSingletons { existing, attempted } => {
write!(
formatter,
"singleton Quadlet keys `{existing}` and `{attempted}` conflict"
)
}
Self::InvalidDocument(diagnostics) => {
write!(
formatter,
"generated Quadlet document has {} diagnostic(s)",
diagnostics.len()
)
}
Self::TypedModel(error) => write!(formatter, "generated Quadlet model is inconsistent: {error}"),
}
}
}
impl Error for RenderError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::TypedModel(error) => Some(error),
_ => None,
}
}
}
const fn container_key_name(key: ContainerKey) -> &'static str {
match key {
ContainerKey::AddHost => "AddHost",
ContainerKey::Image => "Image",
ContainerKey::Exec => "Exec",
ContainerKey::Environment => "Environment",
ContainerKey::EnvironmentFile => "EnvironmentFile",
ContainerKey::Label => "Label",
ContainerKey::Secret => "Secret",
ContainerKey::PublishPort => "PublishPort",
ContainerKey::Volume => "Volume",
ContainerKey::Network => "Network",
ContainerKey::Pod => "Pod",
ContainerKey::HealthCmd => "HealthCmd",
ContainerKey::Notify => "Notify",
ContainerKey::HealthInterval => "HealthInterval",
ContainerKey::HealthRetries => "HealthRetries",
ContainerKey::HealthStartPeriod => "HealthStartPeriod",
ContainerKey::HealthTimeout => "HealthTimeout",
ContainerKey::PodmanArgs => "PodmanArgs",
ContainerKey::User => "User",
ContainerKey::Group => "Group",
ContainerKey::UserNS => "UserNS",
ContainerKey::GroupAdd => "GroupAdd",
ContainerKey::WorkingDir => "WorkingDir",
ContainerKey::ReadOnly => "ReadOnly",
ContainerKey::Rootfs => "Rootfs",
ContainerKey::ContainerName => "ContainerName",
ContainerKey::Entrypoint => "Entrypoint",
ContainerKey::RunInit => "RunInit",
ContainerKey::StopSignal => "StopSignal",
ContainerKey::StopTimeout => "StopTimeout",
ContainerKey::Pull => "Pull",
ContainerKey::PidsLimit => "PidsLimit",
ContainerKey::HostName => "HostName",
ContainerKey::ShmSize => "ShmSize",
ContainerKey::DropCapability => "DropCapability",
ContainerKey::AddCapability => "AddCapability",
ContainerKey::Tmpfs => "Tmpfs",
ContainerKey::Sysctl => "Sysctl",
ContainerKey::Ulimit => "Ulimit",
ContainerKey::AddDevice => "AddDevice",
ContainerKey::Memory => "Memory",
ContainerKey::DNS => "DNS",
ContainerKey::DNSOption => "DNSOption",
ContainerKey::DNSSearch => "DNSSearch",
ContainerKey::ExposeHostPort => "ExposeHostPort",
ContainerKey::Annotation => "Annotation",
ContainerKey::AppArmor => "AppArmor",
ContainerKey::NoNewPrivileges => "NoNewPrivileges",
ContainerKey::SeccompProfile => "SeccompProfile",
ContainerKey::SecurityLabelDisable => "SecurityLabelDisable",
ContainerKey::SecurityLabelFileType => "SecurityLabelFileType",
ContainerKey::SecurityLabelLevel => "SecurityLabelLevel",
ContainerKey::SecurityLabelNested => "SecurityLabelNested",
ContainerKey::SecurityLabelType => "SecurityLabelType",
ContainerKey::Mask => "Mask",
ContainerKey::Unmask => "Unmask",
ContainerKey::LogDriver => "LogDriver",
ContainerKey::LogOpt => "LogOpt",
ContainerKey::IP => "IP",
ContainerKey::IP6 => "IP6",
ContainerKey::NetworkAlias => "NetworkAlias",
ContainerKey::ReloadCmd => "ReloadCmd",
ContainerKey::ReloadSignal => "ReloadSignal",
}
}
const fn build_key_name(key: BuildKey) -> &'static str {
match key {
BuildKey::ImageTag => "ImageTag",
BuildKey::SetWorkingDirectory => "SetWorkingDirectory",
BuildKey::File => "File",
BuildKey::Target => "Target",
BuildKey::Network => "Network",
BuildKey::Label => "Label",
BuildKey::BuildArg => "BuildArg",
BuildKey::Secret => "Secret",
BuildKey::Arch => "Arch",
BuildKey::Variant => "Variant",
BuildKey::Pull => "Pull",
BuildKey::PodmanArgs => "PodmanArgs",
BuildKey::Retry => "Retry",
BuildKey::RetryDelay => "RetryDelay",
BuildKey::TLSVerify => "TLSVerify",
BuildKey::ForceRM => "ForceRM",
BuildKey::GroupAdd => "GroupAdd",
BuildKey::DNS => "DNS",
BuildKey::DNSOption => "DNSOption",
BuildKey::DNSSearch => "DNSSearch",
BuildKey::AuthFile => "AuthFile",
BuildKey::IgnoreFile => "IgnoreFile",
BuildKey::Annotation => "Annotation",
BuildKey::Environment => "Environment",
BuildKey::ContainersConfModule => "ContainersConfModule",
BuildKey::GlobalArgs => "GlobalArgs",
BuildKey::ServiceName => "ServiceName",
BuildKey::Volume => "Volume",
}
}
const fn image_key_name(key: ImageKey) -> &'static str {
match key {
ImageKey::Image => "Image",
ImageKey::ImageTag => "ImageTag",
ImageKey::ServiceName => "ServiceName",
ImageKey::AllTags => "AllTags",
ImageKey::Arch => "Arch",
ImageKey::AuthFile => "AuthFile",
ImageKey::CertDir => "CertDir",
ImageKey::ContainersConfModule => "ContainersConfModule",
ImageKey::Creds => "Creds",
ImageKey::DecryptionKey => "DecryptionKey",
ImageKey::GlobalArgs => "GlobalArgs",
ImageKey::OS => "OS",
}
}
const fn pod_key_name(key: PodKey) -> &'static str {
match key {
PodKey::AddHost => "AddHost",
PodKey::PodName => "PodName",
PodKey::PublishPort => "PublishPort",
PodKey::Network => "Network",
PodKey::Volume => "Volume",
PodKey::UserNS => "UserNS",
PodKey::ShmSize => "ShmSize",
PodKey::ExitPolicy => "ExitPolicy",
PodKey::StopTimeout => "StopTimeout",
PodKey::ServiceName => "ServiceName",
}
}
const fn network_key_name(key: NetworkKey) -> &'static str {
match key {
NetworkKey::NetworkName => "NetworkName",
NetworkKey::Driver => "Driver",
NetworkKey::Options => "Options",
NetworkKey::Internal => "Internal",
NetworkKey::IPv6 => "IPv6",
NetworkKey::IPAMDriver => "IPAMDriver",
NetworkKey::Subnet => "Subnet",
NetworkKey::Gateway => "Gateway",
NetworkKey::IPRange => "IPRange",
NetworkKey::Label => "Label",
}
}
const fn volume_key_name(key: VolumeKey) -> &'static str {
match key {
VolumeKey::VolumeName => "VolumeName",
VolumeKey::Driver => "Driver",
VolumeKey::Options => "Options",
VolumeKey::Label => "Label",
VolumeKey::Device => "Device",
VolumeKey::Type => "Type",
VolumeKey::Copy => "Copy",
VolumeKey::ContainersConfModule => "ContainersConfModule",
VolumeKey::GlobalArgs => "GlobalArgs",
VolumeKey::PodmanArgs => "PodmanArgs",
VolumeKey::User => "User",
VolumeKey::Group => "Group",
VolumeKey::UID => "UID",
VolumeKey::GID => "GID",
VolumeKey::ServiceName => "ServiceName",
VolumeKey::Image => "Image",
}
}
const fn section_name(section: SectionKind) -> &'static str {
match section {
SectionKind::Unit => "Unit",
SectionKind::Container => "Container",
SectionKind::Pod => "Pod",
SectionKind::Network => "Network",
SectionKind::Volume => "Volume",
SectionKind::Build => "Build",
SectionKind::Image => "Image",
SectionKind::Service => "Service",
SectionKind::Install => "Install",
SectionKind::Unknown => "Unknown",
}
}