1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum HookError {
10 Empty,
11 UnknownKind,
12 InvalidEnv,
13 InvalidTimeout,
14}
15
16impl fmt::Display for HookError {
17 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::Empty => formatter.write_str("OCI hook value cannot be empty"),
20 Self::UnknownKind => formatter.write_str("unknown OCI hook kind"),
21 Self::InvalidEnv => formatter.write_str("invalid OCI hook environment entry"),
22 Self::InvalidTimeout => formatter.write_str("invalid OCI hook timeout"),
23 }
24 }
25}
26
27impl Error for HookError {}
28
29#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
31pub enum HookKind {
32 Prestart,
33 CreateRuntime,
34 CreateContainer,
35 StartContainer,
36 Poststart,
37 Poststop,
38}
39
40impl HookKind {
41 #[must_use]
43 pub const fn as_str(self) -> &'static str {
44 match self {
45 Self::Prestart => "prestart",
46 Self::CreateRuntime => "createRuntime",
47 Self::CreateContainer => "createContainer",
48 Self::StartContainer => "startContainer",
49 Self::Poststart => "poststart",
50 Self::Poststop => "poststop",
51 }
52 }
53}
54
55impl fmt::Display for HookKind {
56 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57 formatter.write_str(self.as_str())
58 }
59}
60
61impl FromStr for HookKind {
62 type Err = HookError;
63
64 fn from_str(value: &str) -> Result<Self, Self::Err> {
65 let key = value.trim().replace(['-', '_'], "").to_ascii_lowercase();
66 match key.as_str() {
67 "prestart" => Ok(Self::Prestart),
68 "createruntime" => Ok(Self::CreateRuntime),
69 "createcontainer" => Ok(Self::CreateContainer),
70 "startcontainer" => Ok(Self::StartContainer),
71 "poststart" => Ok(Self::Poststart),
72 "poststop" => Ok(Self::Poststop),
73 "" => Err(HookError::Empty),
74 _ => Err(HookError::UnknownKind),
75 }
76 }
77}
78
79macro_rules! text_part {
80 ($name:ident) => {
81 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
82 pub struct $name(String);
83
84 impl $name {
85 pub fn new(value: impl AsRef<str>) -> Result<Self, HookError> {
87 let trimmed = value.as_ref().trim();
88 if trimmed.is_empty() {
89 return Err(HookError::Empty);
90 }
91 if trimmed.contains('\0') {
92 return Err(HookError::InvalidEnv);
93 }
94 Ok(Self(trimmed.to_string()))
95 }
96
97 #[must_use]
99 pub fn as_str(&self) -> &str {
100 &self.0
101 }
102 }
103
104 impl AsRef<str> for $name {
105 fn as_ref(&self) -> &str {
106 self.as_str()
107 }
108 }
109
110 impl fmt::Display for $name {
111 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
112 formatter.write_str(self.as_str())
113 }
114 }
115 };
116}
117
118text_part!(HookPath);
119text_part!(HookArg);
120text_part!(HookEnv);
121
122#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
124pub struct HookTimeoutSeconds(u32);
125
126impl HookTimeoutSeconds {
127 pub const fn new(value: u32) -> Result<Self, HookError> {
129 if value == 0 {
130 Err(HookError::InvalidTimeout)
131 } else {
132 Ok(Self(value))
133 }
134 }
135
136 #[must_use]
138 pub const fn as_u32(self) -> u32 {
139 self.0
140 }
141}
142
143#[derive(Clone, Debug, Eq, PartialEq)]
145pub struct OciHook {
146 kind: HookKind,
147 path: HookPath,
148 args: Vec<HookArg>,
149 env: Vec<HookEnv>,
150 timeout: Option<HookTimeoutSeconds>,
151}
152
153impl OciHook {
154 #[must_use]
156 pub fn new(kind: HookKind, path: HookPath) -> Self {
157 Self {
158 kind,
159 path,
160 args: Vec::new(),
161 env: Vec::new(),
162 timeout: None,
163 }
164 }
165
166 #[must_use]
168 pub fn with_arg(mut self, arg: HookArg) -> Self {
169 self.args.push(arg);
170 self
171 }
172
173 #[must_use]
175 pub fn with_env(mut self, env: HookEnv) -> Self {
176 self.env.push(env);
177 self
178 }
179
180 #[must_use]
182 pub const fn with_timeout(mut self, timeout: HookTimeoutSeconds) -> Self {
183 self.timeout = Some(timeout);
184 self
185 }
186
187 #[must_use]
189 pub const fn kind(&self) -> HookKind {
190 self.kind
191 }
192
193 #[must_use]
195 pub const fn path(&self) -> &HookPath {
196 &self.path
197 }
198
199 #[must_use]
201 pub fn args(&self) -> &[HookArg] {
202 &self.args
203 }
204
205 #[must_use]
207 pub fn env(&self) -> &[HookEnv] {
208 &self.env
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::{HookArg, HookKind, HookPath, HookTimeoutSeconds, OciHook};
215
216 #[test]
217 fn models_hook_metadata_without_execution() -> Result<(), Box<dyn std::error::Error>> {
218 let hook = OciHook::new(HookKind::Prestart, HookPath::new("/bin/check")?)
219 .with_arg(HookArg::new("--dry-run")?)
220 .with_timeout(HookTimeoutSeconds::new(5)?);
221
222 assert_eq!(hook.kind().to_string(), "prestart");
223 assert_eq!(hook.path().as_str(), "/bin/check");
224 assert_eq!(hook.args().len(), 1);
225 assert_eq!(
226 "create-runtime".parse::<HookKind>()?,
227 HookKind::CreateRuntime
228 );
229 Ok(())
230 }
231}