1use crate::error::{OptItemError, OptionsError};
8use crate::escape::decode_escapes;
9use std::fmt;
10use std::str::FromStr;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[non_exhaustive]
19pub enum OptionClass {
20 Vfs,
23 Filesystem,
26 Userspace,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46pub struct OptItem {
47 name: String,
48 value: Option<String>,
49}
50
51impl OptItem {
52 pub fn new(name: impl Into<String>, value: Option<String>) -> Result<Self, OptItemError> {
58 let name = name.into();
59 if name.is_empty() {
60 return Err(OptItemError::EmptyName);
61 }
62 Ok(OptItem { name, value })
63 }
64
65 pub fn flag(name: impl Into<String>) -> Result<Self, OptItemError> {
71 Self::new(name, None)
72 }
73
74 #[must_use]
76 pub fn name(&self) -> &str {
77 &self.name
78 }
79
80 #[must_use]
82 pub fn value(&self) -> Option<&str> {
83 self.value.as_deref()
84 }
85
86 #[must_use]
90 pub fn class(&self) -> Option<OptionClass> {
91 classify_option(&self.name)
92 }
93
94 #[must_use]
96 pub fn is_known(&self) -> bool {
97 self.class().is_some()
98 }
99}
100
101impl fmt::Display for OptItem {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 match &self.value {
104 Some(v) if v.contains(',') => write!(f, "{}=\"{}\"", self.name, v),
105 Some(v) => write!(f, "{}={}", self.name, v),
106 None => write!(f, "{}", self.name),
107 }
108 }
109}
110
111#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127pub struct Options {
128 items: Vec<OptItem>,
129}
130
131impl Options {
132 #[must_use]
134 pub fn new() -> Self {
135 Options { items: Vec::new() }
136 }
137
138 #[must_use]
143 pub fn defaults() -> Self {
144 Options {
145 items: vec![OptItem {
146 name: "defaults".to_owned(),
147 value: None,
148 }],
149 }
150 }
151
152 pub fn parse(raw: &str) -> Result<Self, OptionsError> {
161 if raw.is_empty() {
162 return Ok(Options::new());
163 }
164 let tokens = split_options(raw);
165 let mut items = Vec::with_capacity(tokens.len());
166 for token in tokens {
167 let decoded = decode_escapes(token);
168 let item = if let Some(eq) = decoded.find('=') {
169 let name = decoded[..eq].to_owned();
170 let raw_value = &decoded[eq + 1..];
171 let value = strip_quotes(raw_value);
172 OptItem::new(name, Some(value.to_owned()))
173 .map_err(|_| OptionsError::EmptyOptionName)?
174 } else {
175 OptItem::flag(decoded).map_err(|_| OptionsError::EmptyOptionName)?
176 };
177 items.push(item);
178 }
179 Ok(Options { items })
180 }
181
182 #[must_use]
184 pub fn is_empty(&self) -> bool {
185 self.items.is_empty()
186 }
187
188 #[must_use]
190 pub fn len(&self) -> usize {
191 self.items.len()
192 }
193
194 #[must_use]
215 pub fn get(&self, name: &str) -> Option<&str> {
216 self.items
217 .iter()
218 .rev()
219 .find(|item| item.name == name)
220 .and_then(|item| item.value.as_deref())
221 }
222
223 #[must_use]
236 pub fn has(&self, name: &str) -> bool {
237 self.items.iter().any(|item| item.name == name)
238 }
239
240 #[must_use]
242 pub fn contains(&self, name: &str) -> bool {
243 self.has(name)
244 }
245
246 pub fn iter(&self) -> impl Iterator<Item = &OptItem> {
248 self.items.iter()
249 }
250
251 #[must_use]
256 pub fn is_readonly(&self) -> bool {
257 for item in self.items.iter().rev() {
258 match item.name.as_str() {
259 "ro" => return true,
260 "rw" => return false,
261 "defaults" => return false,
262 _ => {}
263 }
264 }
265 false
266 }
267
268 #[must_use]
272 pub fn is_noauto(&self) -> bool {
273 for item in self.items.iter().rev() {
274 match item.name.as_str() {
275 "noauto" => return true,
276 "auto" => return false,
277 "defaults" => return false,
278 _ => {}
279 }
280 }
281 false
282 }
283
284 #[must_use]
286 pub fn has_nofail(&self) -> bool {
287 self.has("nofail")
288 }
289
290 #[must_use]
292 pub fn is_netdev(&self) -> bool {
293 self.has("_netdev")
294 }
295
296 #[must_use]
300 pub fn mount_permission(&self) -> MountPermission {
301 for item in self.items.iter().rev() {
302 match item.name.as_str() {
303 "user" => return MountPermission::User,
304 "users" => return MountPermission::Users,
305 "owner" => return MountPermission::Owner,
306 "group" => return MountPermission::Group,
307 "nouser" => return MountPermission::None,
308 "defaults" => return MountPermission::None,
309 _ => {}
310 }
311 }
312 MountPermission::None
313 }
314
315 pub fn set(&mut self, name: &str, value: Option<&str>) -> &mut Self {
333 self.items.retain(|item| item.name != name);
334 self.items.push(OptItem {
335 name: name.to_owned(),
336 value: value.map(|v| v.to_owned()),
337 });
338 self
339 }
340
341 pub fn remove(&mut self, name: &str) -> &mut Self {
345 self.items.retain(|item| item.name != name);
346 self
347 }
348
349 pub fn append(&mut self, item: OptItem) -> &mut Self {
353 self.items.push(item);
354 self
355 }
356
357 pub fn prepend(&mut self, item: OptItem) -> &mut Self {
361 self.items.insert(0, item);
362 self
363 }
364
365 pub fn vfs_options(&self) -> impl Iterator<Item = &OptItem> {
367 self.items
368 .iter()
369 .filter(|item| item.class() == Some(OptionClass::Vfs))
370 }
371
372 pub fn fs_options(&self) -> impl Iterator<Item = &OptItem> {
374 self.items
375 .iter()
376 .filter(|item| item.class() == Some(OptionClass::Filesystem))
377 }
378
379 pub fn user_options(&self) -> impl Iterator<Item = &OptItem> {
381 self.items
382 .iter()
383 .filter(|item| item.class() == Some(OptionClass::Userspace))
384 }
385}
386
387impl FromStr for Options {
388 type Err = OptionsError;
389
390 fn from_str(s: &str) -> Result<Self, Self::Err> {
394 Options::parse(s)
395 }
396}
397
398impl TryFrom<&str> for Options {
402 type Error = OptionsError;
403
404 fn try_from(s: &str) -> Result<Self, Self::Error> {
405 Options::parse(s)
406 }
407}
408
409#[derive(Debug, Clone, Copy, PartialEq, Eq)]
411#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
412#[non_exhaustive]
413pub enum MountPermission {
414 None,
416 User,
418 Users,
420 Owner,
422 Group,
424}
425
426impl fmt::Display for Options {
427 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
428 for (i, item) in self.items.iter().enumerate() {
429 if i > 0 {
430 f.write_str(",")?;
431 }
432 write!(f, "{item}")?;
433 }
434 Ok(())
435 }
436}
437
438fn split_options(raw: &str) -> Vec<&str> {
442 let mut items = Vec::new();
443 let mut start = 0;
444 let mut quote: Option<char> = None;
445 for (i, ch) in raw.char_indices() {
446 match (quote, ch) {
447 (None, '"') => quote = Some('"'),
448 (None, '\'') => quote = Some('\''),
449 (Some(q), c) if c == q => quote = None,
450 (None, ',') => {
451 items.push(&raw[start..i]);
452 start = i + 1;
453 }
454 _ => {}
455 }
456 }
457 items.push(&raw[start..]);
458 items
459}
460
461fn strip_quotes(s: &str) -> &str {
463 let s = s.trim();
464 if s.len() >= 2 {
465 let bytes = s.as_bytes();
466 if (bytes[0] == b'"' && bytes[s.len() - 1] == b'"')
467 || (bytes[0] == b'\'' && bytes[s.len() - 1] == b'\'')
468 {
469 return &s[1..s.len() - 1];
470 }
471 }
472 s
473}
474
475const VFS_OPTIONS: &[&str] = &[
477 "ro",
478 "rw",
479 "exec",
480 "noexec",
481 "suid",
482 "nosuid",
483 "dev",
484 "nodev",
485 "remount",
486 "bind",
487 "rbind",
488 "atime",
489 "noatime",
490 "diratime",
491 "nodiratime",
492 "relatime",
493 "norelatime",
494 "strictatime",
495 "nostrictatime",
496 "symfollow",
497 "nosymfollow",
498 "silent",
499 "loud",
500 "iversion",
501 "noiversion",
502 "shared",
503 "rshared",
504 "slave",
505 "rslave",
506 "private",
507 "rprivate",
508 "unbindable",
509 "runbindable",
510];
511
512const FS_OPTIONS: &[&str] = &["sync", "async", "dirsync"];
514
515const USER_OPTIONS: &[&str] = &[
517 "defaults",
518 "auto",
519 "noauto",
520 "user",
521 "nouser",
522 "users",
523 "owner",
524 "group",
525 "_netdev",
526 "nofail",
527 "loop",
528 "offset",
529 "sizelimit",
530 "encryption",
531 "uhelper",
532 "helper",
533];
534
535fn classify_option(name: &str) -> Option<OptionClass> {
537 if name.starts_with("X-") || name.starts_with("x-") || name == "comment" {
538 return Some(OptionClass::Userspace);
539 }
540 if name.starts_with("verity.") {
541 return Some(OptionClass::Userspace);
542 }
543 if VFS_OPTIONS.contains(&name) {
544 return Some(OptionClass::Vfs);
545 }
546 if FS_OPTIONS.contains(&name) {
547 return Some(OptionClass::Filesystem);
548 }
549 if USER_OPTIONS.contains(&name) {
550 return Some(OptionClass::Userspace);
551 }
552 None
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558
559 #[test]
560 fn split_options_with_quotes() {
561 let result = split_options(r#"context="a,b",noatime"#);
562 assert_eq!(result, vec![r#"context="a,b""#, "noatime"]);
563 }
564
565 #[test]
566 fn split_simple() {
567 assert_eq!(split_options("a,b,c"), vec!["a", "b", "c"]);
568 }
569
570 #[test]
571 fn strip_double_quotes() {
572 assert_eq!(strip_quotes("\"hello\""), "hello");
573 }
574
575 #[test]
576 fn strip_single_quotes() {
577 assert_eq!(strip_quotes("'hello'"), "hello");
578 }
579
580 #[test]
581 fn parse_simple_flag() {
582 let opts = Options::parse("defaults").unwrap();
583 assert!(opts.has("defaults"));
584 }
585
586 #[test]
587 fn parse_multiple_flags() {
588 let opts = Options::parse("rw,noatime,nofail").unwrap();
589 assert!(opts.has("rw"));
590 assert!(opts.has("noatime"));
591 assert!(opts.has("nofail"));
592 }
593
594 #[test]
595 fn parse_key_value() {
596 let opts = Options::parse("size=10G,mode=755").unwrap();
597 assert_eq!(opts.get("size"), Some("10G"));
598 assert_eq!(opts.get("mode"), Some("755"));
599 }
600
601 #[test]
602 fn last_option_wins() {
603 let opts = Options::parse("ro,rw").unwrap();
604 assert!(!opts.is_readonly());
605 let opts2 = Options::parse("rw,ro").unwrap();
606 assert!(opts2.is_readonly());
607 }
608
609 #[test]
610 fn quoted_value_preserves_comma() {
611 let opts =
612 Options::parse(r#"context="system_u:object_r:tmp_t:s0:c127,c456",noatime"#).unwrap();
613 assert_eq!(
614 opts.get("context"),
615 Some("system_u:object_r:tmp_t:s0:c127,c456")
616 );
617 assert!(opts.has("noatime"));
618 }
619
620 #[test]
621 fn single_quoted_value() {
622 let opts = Options::parse("key='value,with,commas'").unwrap();
623 assert_eq!(opts.get("key"), Some("value,with,commas"));
624 }
625
626 #[test]
627 fn parse_empty_options() {
628 let opts = Options::parse("").unwrap();
629 assert!(opts.is_empty());
630 }
631
632 #[test]
633 fn serialize_simple() {
634 let opts = Options::parse("rw,noatime").unwrap();
635 assert_eq!(opts.to_string(), "rw,noatime");
636 }
637
638 #[test]
639 fn serialize_with_value() {
640 let opts = Options::parse("size=10G,mode=755").unwrap();
641 assert_eq!(opts.to_string(), "size=10G,mode=755");
642 }
643
644 #[test]
645 fn serialize_roundtrip() {
646 let inputs = [
647 "defaults",
648 "rw,noatime,nofail",
649 "size=10G,mode=755",
650 "ro,nosuid,nodev",
651 ];
652 for input in inputs {
653 let opts = Options::parse(input).unwrap();
654 assert_eq!(opts.to_string(), input, "roundtrip failed for: {input}");
655 }
656 }
657
658 #[test]
659 fn len_and_is_empty() {
660 let opts = Options::new();
661 assert!(opts.is_empty());
662 assert_eq!(opts.len(), 0);
663
664 let opts = Options::parse("a,b").unwrap();
665 assert_eq!(opts.len(), 2);
666 assert!(!opts.is_empty());
667 }
668
669 #[test]
670 fn contains_works() {
671 let opts = Options::parse("rw,noatime").unwrap();
672 assert!(opts.contains("rw"));
673 assert!(!opts.contains("foobar"));
674 }
675
676 #[test]
677 fn set_adds_option() {
678 let mut opts = Options::parse("rw").unwrap();
679 opts.set("noatime", None);
680 assert!(opts.has("noatime"));
681 }
682
683 #[test]
684 fn remove_option() {
685 let mut opts = Options::parse("rw,noatime,nofail").unwrap();
686 opts.remove("noatime");
687 assert!(!opts.has("noatime"));
688 assert!(opts.has("rw"));
689 assert!(opts.has("nofail"));
690 }
691
692 #[test]
693 fn append_option() {
694 let mut opts = Options::parse("rw").unwrap();
695 opts.append(OptItem::flag("noatime").unwrap());
696 assert_eq!(opts.to_string(), "rw,noatime");
697 }
698
699 #[test]
700 fn is_readonly() {
701 assert!(Options::parse("ro").unwrap().is_readonly());
702 assert!(!Options::parse("rw").unwrap().is_readonly());
703 }
704
705 #[test]
706 fn has_nofail() {
707 assert!(Options::parse("nofail").unwrap().has_nofail());
708 assert!(!Options::parse("defaults").unwrap().has_nofail());
709 }
710
711 #[test]
712 fn is_netdev() {
713 assert!(Options::parse("_netdev").unwrap().is_netdev());
714 assert!(!Options::parse("defaults").unwrap().is_netdev());
715 }
716
717 #[test]
718 fn option_classification() {
719 let opts = Options::parse("ro,noexec").unwrap();
720 for item in opts.vfs_options() {
721 assert_eq!(item.class(), Some(OptionClass::Vfs));
722 }
723 let opts = Options::parse("sync").unwrap();
724 for item in opts.fs_options() {
725 assert_eq!(item.class(), Some(OptionClass::Filesystem));
726 }
727 let opts = Options::parse("nofail,_netdev").unwrap();
728 for item in opts.user_options() {
729 assert_eq!(item.class(), Some(OptionClass::Userspace));
730 }
731 }
732
733 #[test]
734 fn unknown_option_is_none_class() {
735 let item = OptItem::flag("mycustomopt").unwrap();
736 assert_eq!(item.class(), None);
737 }
738
739 #[test]
740 fn optitem_empty_name_is_error() {
741 assert!(OptItem::flag("").is_err());
742 }
743
744 #[test]
745 fn iter_preserves_order() {
746 let opts = Options::parse("a,b,c,d").unwrap();
747 let names: Vec<&str> = opts.iter().map(|i| i.name()).collect();
748 assert_eq!(names, vec!["a", "b", "c", "d"]);
749 }
750
751 #[test]
752 fn defaults_constructor() {
753 let opts = Options::defaults();
754 assert!(opts.has("defaults"));
755 }
756
757 #[test]
758 fn from_str_works() {
759 let opts: Options = "rw,noatime".parse().unwrap();
760 assert!(opts.has("rw"));
761 assert!(opts.has("noatime"));
762 }
763
764 #[test]
765 fn set_returns_self_for_chaining() {
766 let mut opts = Options::parse("rw").unwrap();
767 opts.set("noatime", None).set("nofail", None);
768 assert!(opts.has("rw"));
769 assert!(opts.has("noatime"));
770 assert!(opts.has("nofail"));
771 }
772
773 #[test]
774 fn remove_returns_self_for_chaining() {
775 let mut opts = Options::parse("a,b,c").unwrap();
776 opts.remove("a").remove("b");
777 let names: Vec<&str> = opts.iter().map(|o| o.name()).collect();
778 assert_eq!(names, vec!["c"]);
779 }
780}