use crate::error::SpecError;
use crate::escape::decode_escapes;
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
impl TryFrom<&str> for Spec {
type Error = SpecError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Spec::parse(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Spec {
Device(PathBuf),
Label(String),
Uuid(String),
PartLabel(String),
PartUuid(String),
#[deprecated = "ID= tag is not strictly defined and depends on udev rules/hardware"]
Id(String),
NetworkMount {
host: String,
path: PathBuf,
},
Keyword(String),
}
impl Spec {
pub fn parse(raw: &str) -> Result<Self, SpecError> {
if raw.is_empty() {
return Err(SpecError::Empty);
}
let tag_prefixes = ["LABEL=", "UUID=", "PARTLABEL=", "PARTUUID=", "ID="];
for &prefix in &tag_prefixes {
if raw
.get(..prefix.len())
.is_some_and(|s| s.eq_ignore_ascii_case(prefix))
{
let val = &raw[prefix.len()..];
return match prefix {
"LABEL=" => Ok(Spec::Label(val.to_owned())),
"UUID=" => Ok(Spec::Uuid(val.to_owned())),
"PARTLABEL=" => Ok(Spec::PartLabel(val.to_owned())),
"PARTUUID=" => Ok(Spec::PartUuid(val.to_owned())),
"ID=" => {
#[allow(deprecated)]
let result = Ok(Spec::Id(val.to_owned()));
#[allow(deprecated)]
result
}
_ => return Ok(Spec::Keyword(raw.to_owned())),
};
}
}
if raw.starts_with('/') {
return Ok(Spec::Device(PathBuf::from(raw)));
}
if raw.starts_with('[')
&& let Some(bracket_end) = raw.find("]:")
{
let host = &raw[..bracket_end + 1];
let path = &raw[bracket_end + 2..];
if !host.is_empty() {
return Ok(Spec::NetworkMount {
host: host.to_owned(),
path: PathBuf::from(path),
});
}
}
if let Some(colon) = raw.find(':') {
let host = &raw[..colon];
let path = &raw[colon + 1..];
if !host.is_empty() {
return Ok(Spec::NetworkMount {
host: host.to_owned(),
path: PathBuf::from(path),
});
}
}
Ok(Spec::Keyword(raw.to_owned()))
}
pub(crate) fn parse_raw(raw: &str) -> Result<Self, SpecError> {
let decoded = decode_escapes(raw);
Self::parse(&decoded)
}
#[must_use]
#[allow(deprecated)]
pub fn is_tag(&self) -> bool {
matches!(
self,
Spec::Label(_) | Spec::Uuid(_) | Spec::PartLabel(_) | Spec::PartUuid(_) | Spec::Id(_)
)
}
#[must_use]
pub fn is_pseudo(&self) -> bool {
matches!(self, Spec::Keyword(_))
}
#[must_use]
#[allow(deprecated)]
pub fn tag_name(&self) -> Option<&'static str> {
match self {
Spec::Label(_) => Some("LABEL"),
Spec::Uuid(_) => Some("UUID"),
Spec::PartLabel(_) => Some("PARTLABEL"),
Spec::PartUuid(_) => Some("PARTUUID"),
Spec::Id(_) => Some("ID"),
_ => None,
}
}
#[must_use]
#[allow(deprecated)]
pub fn tag_value(&self) -> Option<&str> {
match self {
Spec::Label(v) | Spec::Uuid(v) | Spec::PartLabel(v) | Spec::PartUuid(v) => Some(v),
Spec::Id(v) => Some(v),
_ => None,
}
}
}
impl fmt::Display for Spec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Spec::Device(p) => write!(f, "{}", p.display()),
Spec::Label(v) => write!(f, "LABEL={v}"),
Spec::Uuid(v) => write!(f, "UUID={v}"),
Spec::PartLabel(v) => write!(f, "PARTLABEL={v}"),
Spec::PartUuid(v) => write!(f, "PARTUUID={v}"),
#[allow(deprecated)]
Spec::Id(v) => write!(f, "ID={v}"),
Spec::NetworkMount { host, path } => write!(f, "{host}:{}", path.display()),
Spec::Keyword(v) => write!(f, "{v}"),
}
}
}
impl FromStr for Spec {
type Err = SpecError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Spec::parse(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn parse_device_path() {
let spec = Spec::parse("/dev/sda1").unwrap();
assert_eq!(spec, Spec::Device(PathBuf::from("/dev/sda1")));
}
#[test]
fn parse_device_by_uuid_path() {
let spec = Spec::parse("/dev/disk/by-uuid/abc-123").unwrap();
assert_eq!(
spec,
Spec::Device(PathBuf::from("/dev/disk/by-uuid/abc-123"))
);
}
#[test]
fn parse_label() {
let spec = Spec::parse("LABEL=Boot").unwrap();
assert_eq!(spec, Spec::Label("Boot".to_owned()));
}
#[test]
fn parse_label_with_escape() {
let spec = Spec::parse(r"LABEL=My\040Drive").unwrap();
assert_eq!(spec, Spec::Label(r"My\040Drive".to_owned()));
}
#[test]
fn parse_label_with_space() {
let spec = Spec::parse("LABEL=My Drive").unwrap();
assert_eq!(spec, Spec::Label("My Drive".to_owned()));
}
#[test]
fn parse_label_case_insensitive() {
let spec = Spec::parse("label=Boot").unwrap();
assert_eq!(spec, Spec::Label("Boot".to_owned()));
}
#[test]
fn parse_uuid_case_insensitive() {
let spec = Spec::parse("uuid=abc-123").unwrap();
assert_eq!(spec, Spec::Uuid("abc-123".to_owned()));
}
#[test]
fn parse_partlabel_case_insensitive() {
let spec = Spec::parse("partlabel=System").unwrap();
assert_eq!(spec, Spec::PartLabel("System".to_owned()));
}
#[test]
fn parse_partuuid_case_insensitive() {
let spec = Spec::parse("partuuid=abc-123").unwrap();
assert_eq!(spec, Spec::PartUuid("abc-123".to_owned()));
}
#[test]
#[allow(deprecated)]
fn parse_id_case_insensitive() {
let spec = Spec::parse("id=ata-SAMSUNG_SSD_1234").unwrap();
assert_eq!(spec, Spec::Id("ata-SAMSUNG_SSD_1234".to_owned()));
}
#[test]
fn parse_uuid() {
let spec = Spec::parse("UUID=3e6be9de-8139-11d1-9106-a43f08d823a6").unwrap();
assert_eq!(
spec,
Spec::Uuid("3e6be9de-8139-11d1-9106-a43f08d823a6".to_owned())
);
}
#[test]
fn parse_uuid_fat_uppercase_preserved() {
let spec = Spec::parse("UUID=A40D-85E7").unwrap();
assert_eq!(spec, Spec::Uuid("A40D-85E7".to_owned()));
}
#[test]
fn parse_partlabel() {
let spec = Spec::parse("PARTLABEL=System").unwrap();
assert_eq!(spec, Spec::PartLabel("System".to_owned()));
}
#[test]
fn parse_partuuid() {
let spec = Spec::parse("PARTUUID=d091fd20-162a-43e1-ad50-0e3ce36ab051").unwrap();
assert_eq!(
spec,
Spec::PartUuid("d091fd20-162a-43e1-ad50-0e3ce36ab051".to_owned())
);
}
#[test]
#[allow(deprecated)]
fn parse_id() {
let spec = Spec::parse("ID=ata-SAMSUNG_SSD_1234").unwrap();
assert_eq!(spec, Spec::Id("ata-SAMSUNG_SSD_1234".to_owned()));
}
#[test]
fn parse_nfs_mount() {
let spec = Spec::parse("server.example.com:/exports/data").unwrap();
assert_eq!(
spec,
Spec::NetworkMount {
host: "server.example.com".to_owned(),
path: PathBuf::from("/exports/data"),
}
);
}
#[test]
fn parse_nfs_mount_ip() {
let spec = Spec::parse("192.168.1.1:/nfs/share").unwrap();
assert_eq!(
spec,
Spec::NetworkMount {
host: "192.168.1.1".to_owned(),
path: PathBuf::from("/nfs/share"),
}
);
}
#[test]
fn parse_keyword_proc() {
let spec = Spec::parse("proc").unwrap();
assert_eq!(spec, Spec::Keyword("proc".to_owned()));
}
#[test]
fn parse_keyword_none() {
let spec = Spec::parse("none").unwrap();
assert_eq!(spec, Spec::Keyword("none".to_owned()));
}
#[test]
fn parse_keyword_tmpfs() {
let spec = Spec::parse("tmpfs").unwrap();
assert_eq!(spec, Spec::Keyword("tmpfs".to_owned()));
}
#[test]
fn parse_empty_is_error() {
assert!(Spec::parse("").is_err());
}
#[test]
#[allow(deprecated)]
fn spec_is_tag() {
assert!(Spec::Label("test".into()).is_tag());
assert!(Spec::Uuid("test".into()).is_tag());
assert!(Spec::PartLabel("test".into()).is_tag());
assert!(Spec::PartUuid("test".into()).is_tag());
assert!(Spec::Id("test".into()).is_tag());
assert!(!Spec::Device(PathBuf::from("/dev/sda")).is_tag());
assert!(!Spec::Keyword("proc".into()).is_tag());
}
#[test]
fn parse_nfs_with_ipv6_brackets() {
let spec = Spec::parse("[::1]:/nfs/share").unwrap();
assert_eq!(
spec,
Spec::NetworkMount {
host: "[::1]".into(),
path: PathBuf::from("/nfs/share"),
}
);
}
#[test]
fn parse_label_with_escape_roundtrip() {
let spec = Spec::parse_raw(r"LABEL=My\040Drive").unwrap();
assert_eq!(spec, Spec::Label("My Drive".to_owned()));
}
#[test]
fn from_str_works() {
let spec: Spec = "UUID=abc".parse().unwrap();
assert_eq!(spec, Spec::Uuid("abc".to_owned()));
}
#[test]
fn parse_emoji_does_not_panic() {
let spec = Spec::parse("🐈").unwrap();
assert!(matches!(spec, Spec::Keyword(_)));
}
}