use std::borrow::Cow;
use std::fmt;
use std::num::NonZeroU16;
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use serde::de::{self, MapAccess, SeqAccess, Visitor};
use serde::{Deserialize, Deserializer};
use super::super::mistake::Mistake;
use super::super::names::PortName;
use super::super::problem::Code;
use super::words::{AbsPath, UnitName, UserName};
use crate::model::Protocol;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Argv(Vec<String>);
impl Argv {
#[must_use]
pub fn args(&self) -> &[String] {
&self.0
}
#[must_use]
pub fn program(&self) -> &str {
self.0.first().map_or("", String::as_str)
}
}
impl TryFrom<Vec<String>> for Argv {
type Error = String;
fn try_from(args: Vec<String>) -> Result<Argv, String> {
match args.first() {
Some(program) if !program.trim().is_empty() => Ok(Argv(args)),
_ => Err("a command starts with its program (`[/usr/sbin/myd, --foreground]`)".to_owned()),
}
}
}
impl<'de> Deserialize<'de> for Argv {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Argv, D::Error> {
Argv::try_from(Vec::<String>::deserialize(d)?).map_err(de::Error::custom)
}
}
impl JsonSchema for Argv {
fn schema_name() -> Cow<'static, str> {
"Argv".into()
}
fn inline_schema() -> bool {
true
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
json_schema!({ "type": "array", "items": { "type": "string" }, "minItems": 1 })
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Invocation {
Shell(String),
Exec(Argv),
}
impl<'de> Deserialize<'de> for Invocation {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Invocation, D::Error> {
d.deserialize_any(InvocationVisitor)
}
}
struct InvocationVisitor;
impl<'de> Visitor<'de> for InvocationVisitor {
type Value = Invocation;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a text the shell runs, or a list of arguments")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Invocation, E> {
if v.trim().is_empty() {
return Err(E::custom("a command says what it runs"));
}
Ok(Invocation::Shell(v.to_owned()))
}
fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<Invocation, A::Error> {
Argv::deserialize(de::value::SeqAccessDeserializer::new(seq)).map(Invocation::Exec)
}
}
impl JsonSchema for Invocation {
fn schema_name() -> Cow<'static, str> {
"Invocation".into()
}
fn inline_schema() -> bool {
true
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
json_schema!({ "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "array", "items": { "type": "string" }, "minItems": 1 }] })
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Guard {
Creates(AbsPath),
Unless(Invocation),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CommandStep {
pub run: Invocation,
pub guard: Guard,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[serde(try_from = "ServiceInput")]
#[non_exhaustive]
pub enum Service {
Unit {
name: UnitName,
enabled: bool,
},
Defined {
name: UnitName,
run: Argv,
user: Option<UserName>,
cwd: Option<AbsPath>,
enabled: bool,
},
}
impl Service {
#[must_use]
pub fn name(&self) -> &UnitName {
match self {
Service::Unit { name, .. } | Service::Defined { name, .. } => name,
}
}
#[must_use]
pub fn enabled(&self) -> bool {
match self {
Service::Unit { enabled, .. } | Service::Defined { enabled, .. } => *enabled,
}
}
}
#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
struct ServiceFull {
name: UnitName,
#[serde(default)]
run: Option<Argv>,
#[serde(default)]
user: Option<UserName>,
#[serde(default)]
cwd: Option<AbsPath>,
#[serde(default)]
enabled: Option<bool>,
}
#[derive(JsonSchema)]
#[serde(untagged)]
enum ServiceInput {
Unit(UnitName),
Full(ServiceFull),
}
schema_as!(Service, ServiceInput);
impl<'de> Deserialize<'de> for ServiceInput {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<ServiceInput, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = ServiceInput;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a unit's name, or `{ name, run, user, cwd, enabled }`")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<ServiceInput, E> {
v.parse().map(ServiceInput::Unit).map_err(E::custom)
}
fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<ServiceInput, A::Error> {
ServiceFull::deserialize(de::value::MapAccessDeserializer::new(map)).map(ServiceInput::Full)
}
}
d.deserialize_any(V)
}
}
impl TryFrom<ServiceInput> for Service {
type Error = Mistake;
fn try_from(written: ServiceInput) -> Result<Service, Mistake> {
let s = match written {
ServiceInput::Unit(name) => return Ok(Service::Unit { name, enabled: true }),
ServiceInput::Full(s) => s,
};
let enabled = s.enabled.unwrap_or(true);
match s.run {
Some(run) => Ok(Service::Defined {
name: s.name,
run,
user: s.user,
cwd: s.cwd,
enabled,
}),
None if s.user.is_some() || s.cwd.is_some() => {
let field = if s.user.is_some() { "user" } else { "cwd" };
Err(
Mistake::new(Code::Conflict, format!("`{field}` goes with `run`: a package's unit runs as its package says"))
.at(field)
.fix("`run: [<program>, <args>...]` to define the service, or leave it out"),
)
}
None => Ok(Service::Unit { name: s.name, enabled }),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Firewall {
pub allow: Vec<FirewallPort>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FirewallPort {
Named(PortName),
Number {
port: NonZeroU16,
protocol: Protocol,
},
}
impl<'de> Deserialize<'de> for FirewallPort {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<FirewallPort, D::Error> {
d.deserialize_any(PortVisitor)
}
}
struct PortVisitor;
impl Visitor<'_> for PortVisitor {
type Value = FirewallPort;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a port's name, a number, or `<number>/udp`")
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<FirewallPort, E> {
number(&v.to_string(), Protocol::Tcp)
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<FirewallPort, E> {
number(&v.to_string(), Protocol::Tcp)
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<FirewallPort, E> {
match v.split_once('/') {
Some((n, "udp")) => number(n, Protocol::Udp),
Some((n, "tcp")) => number(n, Protocol::Tcp),
Some(_) => Err(E::custom(format!("`{v}`: a port is `<number>`, `<number>/udp` or `<number>/tcp`"))),
None if v.starts_with(|c: char| c.is_ascii_digit()) => number(v, Protocol::Tcp),
None => v.parse().map(FirewallPort::Named).map_err(E::custom),
}
}
}
fn number<E: de::Error>(n: &str, protocol: Protocol) -> Result<FirewallPort, E> {
n.parse::<NonZeroU16>()
.map(|port| FirewallPort::Number { port, protocol })
.map_err(|_| E::custom(format!("`{n}` isn't a port: 1 to 65535")))
}
impl JsonSchema for FirewallPort {
fn schema_name() -> Cow<'static, str> {
"FirewallPort".into()
}
fn inline_schema() -> bool {
true
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
json_schema!({ "anyOf": [
{ "type": "integer", "minimum": 1, "maximum": 65535 },
{ "type": "string", "pattern": "^([a-z][a-z0-9-]{0,30}|[0-9]{1,5}(/(udp|tcp))?)$" },
] })
}
}