1use std::{
5 fmt::{self, Display, Formatter, Write},
6 num::ParseIntError,
7 path::PathBuf,
8 str::FromStr,
9};
10
11use compose_spec_macros::{DeserializeFromStr, SerializeDisplay};
12use thiserror::Error;
13
14use super::{volumes::AbsolutePathError, AbsolutePath};
15
16#[derive(SerializeDisplay, DeserializeFromStr, Debug, Clone, PartialEq, Eq, Hash)]
23pub struct Device {
24 pub host_path: PathBuf,
26
27 pub container_path: AbsolutePath,
29
30 pub permissions: Permissions,
32}
33
34impl FromStr for Device {
35 type Err = ParseDeviceError;
36
37 fn from_str(s: &str) -> Result<Self, Self::Err> {
38 let mut split = s.splitn(3, ':');
40
41 let host_path = split.next().ok_or(ParseDeviceError::Empty)?.into();
42 let container_path = split
43 .next()
44 .ok_or(ParseDeviceError::ContainerPathMissing)?
45 .parse()?;
46 let permissions = split.next().unwrap_or_default().parse()?;
47
48 Ok(Self {
49 host_path,
50 container_path,
51 permissions,
52 })
53 }
54}
55
56impl TryFrom<&str> for Device {
57 type Error = ParseDeviceError;
58
59 fn try_from(value: &str) -> Result<Self, Self::Error> {
60 value.parse()
61 }
62}
63
64#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
66pub enum ParseDeviceError {
67 #[error("device cannot be an empty string")]
69 Empty,
70
71 #[error("device must have a container path")]
73 ContainerPathMissing,
74
75 #[error("device container path must be absolute")]
77 ContainerPathAbsolute(#[from] AbsolutePathError),
78
79 #[error("error parsing device permissions")]
81 Permissions(#[from] ParsePermissionsError),
82}
83
84impl Display for Device {
85 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
86 let Self {
87 host_path,
88 container_path,
89 permissions,
90 } = self;
91
92 write!(
93 f,
94 "{}:{}",
95 host_path.display(),
96 container_path.as_path().display(),
97 )?;
98
99 if permissions.any() {
100 write!(f, ":{permissions}")?;
101 }
102
103 Ok(())
104 }
105}
106
107#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
109pub struct Permissions {
110 pub read: bool,
112
113 pub write: bool,
115
116 pub mknod: bool,
118}
119
120impl Permissions {
121 #[must_use]
123 pub const fn all() -> Self {
124 Self {
125 read: true,
126 write: true,
127 mknod: true,
128 }
129 }
130
131 #[must_use]
133 pub const fn any(self) -> bool {
134 let Self { read, write, mknod } = self;
135 read || write || mknod
136 }
137}
138
139impl FromStr for Permissions {
140 type Err = ParsePermissionsError;
141
142 fn from_str(s: &str) -> Result<Self, Self::Err> {
143 let mut read = false;
144 let mut write = false;
145 let mut mknod = false;
146
147 for permission in s.chars() {
148 match permission {
149 'r' => read = true,
150 'w' => write = true,
151 'm' => mknod = true,
152 unknown => return Err(ParsePermissionsError(unknown)),
153 }
154 }
155
156 Ok(Self { read, write, mknod })
157 }
158}
159
160impl TryFrom<&str> for Permissions {
161 type Error = ParsePermissionsError;
162
163 fn try_from(value: &str) -> Result<Self, Self::Error> {
164 value.parse()
165 }
166}
167
168#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
170#[error("invalid device permission `{0}`, must be `r` (read), `w` (write), or `m` (mknod)")]
171pub struct ParsePermissionsError(char);
172
173impl Display for Permissions {
174 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
175 let Self { read, write, mknod } = *self;
176
177 if read {
178 f.write_char('r')?;
179 }
180 if write {
181 f.write_char('w')?;
182 }
183 if mknod {
184 f.write_char('m')?;
185 }
186
187 Ok(())
188 }
189}
190
191#[derive(SerializeDisplay, DeserializeFromStr, Debug, Clone, Copy, PartialEq, Eq, Hash)]
199pub struct CgroupRule {
200 pub kind: Kind,
202
203 pub major: MajorMinorNumber,
205
206 pub minor: MajorMinorNumber,
208
209 pub permissions: Permissions,
211}
212
213impl FromStr for CgroupRule {
214 type Err = ParseCgroupRuleError;
215
216 fn from_str(s: &str) -> Result<Self, Self::Err> {
217 let mut split = s.splitn(3, ' ');
220
221 let kind = split.next().ok_or(ParseCgroupRuleError::Empty)?.parse()?;
222
223 let (major, minor) = split
224 .next()
225 .and_then(|s| s.split_once(':'))
226 .ok_or(ParseCgroupRuleError::MajorMinorNumbersMissing)?;
227 let major = major.parse()?;
228 let minor = minor.parse()?;
229
230 let permissions = split.next().unwrap_or_default().parse()?;
231
232 Ok(Self {
233 kind,
234 major,
235 minor,
236 permissions,
237 })
238 }
239}
240
241impl TryFrom<&str> for CgroupRule {
242 type Error = ParseCgroupRuleError;
243
244 fn try_from(value: &str) -> Result<Self, Self::Error> {
245 value.parse()
246 }
247}
248
249#[derive(Error, Debug, Clone, PartialEq, Eq)]
251pub enum ParseCgroupRuleError {
252 #[error("device cgroup rule cannot be empty")]
254 Empty,
255
256 #[error("invalid device kind")]
258 Kind(#[from] ParseKindError),
259
260 #[error("device cgroup rule missing major minor numbers")]
262 MajorMinorNumbersMissing,
263
264 #[error("error parsing device major minor number")]
266 MajorMinorNumber(#[from] ParseIntError),
267
268 #[error("error parsing device cgroup rule permissions")]
270 Permissions(#[from] ParsePermissionsError),
271}
272
273impl Display for CgroupRule {
274 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
275 let Self {
276 kind,
277 major,
278 minor,
279 permissions,
280 } = self;
281
282 write!(f, "{kind} {major}:{minor}")?;
285
286 if permissions.any() {
287 write!(f, " {permissions}")?;
288 }
289
290 Ok(())
291 }
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
296pub enum Kind {
297 All,
299
300 Char,
302
303 Block,
305}
306
307impl Kind {
308 #[must_use]
310 pub const fn as_char(self) -> char {
311 match self {
312 Self::All => 'a',
313 Self::Char => 'c',
314 Self::Block => 'b',
315 }
316 }
317}
318
319impl TryFrom<char> for Kind {
320 type Error = ParseKindError;
321
322 fn try_from(value: char) -> Result<Self, Self::Error> {
323 match value {
324 'a' => Ok(Self::All),
325 'c' => Ok(Self::Char),
326 'b' => Ok(Self::Block),
327 unknown => Err(ParseKindError(unknown.into())),
328 }
329 }
330}
331
332impl FromStr for Kind {
333 type Err = ParseKindError;
334
335 fn from_str(s: &str) -> Result<Self, Self::Err> {
336 match s {
337 "a" => Ok(Self::All),
338 "c" => Ok(Self::Char),
339 "b" => Ok(Self::Block),
340 unknown => Err(ParseKindError(unknown.to_owned())),
341 }
342 }
343}
344
345impl TryFrom<&str> for Kind {
346 type Error = ParseKindError;
347
348 fn try_from(value: &str) -> Result<Self, Self::Error> {
349 value.parse()
350 }
351}
352
353#[derive(Error, Debug, Clone, PartialEq, Eq)]
355#[error("invalid device kind `{0}`, must be `a` (all), `c` (char), or `b` (block)")]
356pub struct ParseKindError(String);
357
358impl Display for Kind {
359 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
360 f.write_char(self.as_char())
361 }
362}
363
364impl From<Kind> for char {
365 fn from(value: Kind) -> Self {
366 value.as_char()
367 }
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
372pub enum MajorMinorNumber {
373 All,
375
376 Integer(u16),
378}
379
380impl PartialEq<u16> for MajorMinorNumber {
381 fn eq(&self, other: &u16) -> bool {
382 match self {
383 Self::All => false,
384 Self::Integer(num) => num.eq(other),
385 }
386 }
387}
388
389impl From<u16> for MajorMinorNumber {
390 fn from(value: u16) -> Self {
391 Self::Integer(value)
392 }
393}
394
395impl FromStr for MajorMinorNumber {
396 type Err = ParseIntError;
397
398 fn from_str(s: &str) -> Result<Self, Self::Err> {
399 if s.is_empty() || s == "*" {
400 Ok(Self::All)
401 } else {
402 s.parse().map(Self::Integer)
403 }
404 }
405}
406
407impl TryFrom<&str> for MajorMinorNumber {
408 type Error = ParseIntError;
409
410 fn try_from(value: &str) -> Result<Self, Self::Error> {
411 value.parse()
412 }
413}
414
415impl Display for MajorMinorNumber {
416 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
417 match self {
418 Self::All => f.write_char('*'),
419 Self::Integer(num) => Display::fmt(num, f),
420 }
421 }
422}
423
424#[cfg(test)]
425#[allow(clippy::unwrap_used)]
426mod tests {
427 use proptest::{
428 arbitrary::any,
429 prop_assert_eq, prop_compose, prop_oneof, proptest,
430 strategy::{Just, Strategy},
431 };
432
433 use super::*;
434
435 mod device {
436 use crate::service::tests::path_no_colon;
437
438 use super::*;
439
440 #[test]
441 fn from_str() {
442 let device = Device {
443 host_path: "/host".into(),
444 container_path: "/container".parse().unwrap(),
445 permissions: Permissions {
446 read: true,
447 write: true,
448 mknod: false,
449 },
450 };
451 assert_eq!(device, "/host:/container:rw".parse().unwrap());
452 }
453
454 #[test]
455 fn display() {
456 let device = Device {
457 host_path: "/host".into(),
458 container_path: "/container".parse().unwrap(),
459 permissions: Permissions {
460 read: true,
461 write: true,
462 mknod: false,
463 },
464 };
465 assert_eq!(device.to_string(), "/host:/container:rw");
466 }
467
468 proptest! {
469 #[test]
470 fn parse_no_panic(string: String) {
471 let _ = string.parse::<Device>();
472 }
473
474 #[test]
475 fn to_string_no_panic(device in device()) {
476 device.to_string();
477 }
478
479 #[test]
480 fn round_trip(device in device()) {
481 prop_assert_eq!(&device, &device.to_string().parse()?);
482 }
483 }
484
485 prop_compose! {
486 fn device()(
487 host_path in path_no_colon(),
488 container_path: AbsolutePath,
489 permissions in permissions()
490 ) -> Device {
491 Device { host_path, container_path, permissions }
492 }
493 }
494 }
495
496 mod permissions {
497 use super::*;
498
499 #[test]
500 fn from_str() {
501 assert_eq!(Permissions::default(), "".parse().unwrap());
502 assert_eq!(
503 Permissions {
504 read: true,
505 write: true,
506 mknod: true
507 },
508 "rwm".parse().unwrap(),
509 );
510 }
511
512 #[test]
513 fn display() {
514 assert!(Permissions::default().to_string().is_empty());
515 assert_eq!(
516 Permissions {
517 read: true,
518 write: true,
519 mknod: true
520 }
521 .to_string(),
522 "rwm",
523 );
524 }
525
526 proptest! {
527 #[test]
528 fn parse_no_panic(string: String) {
529 let _ = string.parse::<Permissions>();
530 }
531
532 #[test]
533 fn to_string_no_panic(permissions in permissions()) {
534 permissions.to_string();
535 }
536
537 #[test]
538 fn round_trip(permissions in permissions()) {
539 prop_assert_eq!(permissions, permissions.to_string().parse()?);
540 }
541 }
542 }
543
544 mod cgroup_rule {
545 use super::*;
546
547 #[test]
548 fn from_str() {
549 let rule = CgroupRule {
550 kind: Kind::Char,
551 major: MajorMinorNumber::Integer(1),
552 minor: MajorMinorNumber::Integer(3),
553 permissions: Permissions {
554 read: true,
555 write: false,
556 mknod: true,
557 },
558 };
559 assert_eq!(rule, "c 1:3 mr".parse().unwrap());
560
561 let rule = CgroupRule {
562 kind: Kind::All,
563 major: MajorMinorNumber::Integer(7),
564 minor: MajorMinorNumber::All,
565 permissions: Permissions::all(),
566 };
567 assert_eq!(rule, "a 7:* rmw".parse().unwrap());
568 }
569
570 #[test]
571 fn display() {
572 let rule = CgroupRule {
573 kind: Kind::Char,
574 major: MajorMinorNumber::Integer(1),
575 minor: MajorMinorNumber::Integer(3),
576 permissions: Permissions {
577 read: true,
578 write: false,
579 mknod: true,
580 },
581 };
582 assert_eq!(rule.to_string(), "c 1:3 rm");
583
584 let rule = CgroupRule {
585 kind: Kind::All,
586 major: MajorMinorNumber::Integer(7),
587 minor: MajorMinorNumber::All,
588 permissions: Permissions::all(),
589 };
590 assert_eq!(rule.to_string(), "a 7:* rwm");
591 }
592
593 proptest! {
594 #[test]
595 fn parse_no_panic(string: String) {
596 let _ = string.parse::<CgroupRule>();
597 }
598
599 #[test]
600 fn to_string_no_panic(rule in cgroup_rule()) {
601 rule.to_string();
602 }
603
604 #[test]
605 fn round_trip(rule in cgroup_rule()) {
606 prop_assert_eq!(rule, rule.to_string().parse()?);
607 }
608 }
609
610 prop_compose! {
611 fn cgroup_rule()(
612 kind in kind(),
613 major in major_minor_number(),
614 minor in major_minor_number(),
615 permissions in permissions(),
616 ) -> CgroupRule {
617 CgroupRule {
618 kind,
619 major,
620 minor,
621 permissions,
622 }
623 }
624 }
625
626 fn kind() -> impl Strategy<Value = Kind> {
627 prop_oneof![Just(Kind::All), Just(Kind::Char), Just(Kind::Block)]
628 }
629
630 fn major_minor_number() -> impl Strategy<Value = MajorMinorNumber> {
631 prop_oneof![
632 1 => Just(MajorMinorNumber::All),
633 u16::MAX.into() => any::<u16>().prop_map_into(),
634 ]
635 }
636 }
637
638 prop_compose! {
639 fn permissions()(read: bool, write: bool, mknod: bool) -> Permissions {
640 Permissions { read, write, mknod }
641 }
642 }
643}