Skip to main content

compose_lens/model/
device.rs

1//! Raw-preserving service device declarations.
2
3use crate::source::SourceSpan;
4
5use super::{FieldReference, Located};
6
7/// One scalar short-syntax service device declaration.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct ShortDevice {
10    raw: Located<String>,
11    kind: ShortDeviceKind,
12}
13
14impl ShortDevice {
15    pub(crate) fn new(raw: Located<String>) -> Self {
16        let kind = classify_short_device(raw.value());
17        Self { raw, kind }
18    }
19
20    /// Returns the complete scalar without parsing or normalizing colon-delimited components.
21    #[must_use]
22    pub const fn raw(&self) -> &Located<String> {
23        &self.raw
24    }
25
26    /// Returns a conservative lexical family that makes no runtime-support claim.
27    #[must_use]
28    pub const fn kind(&self) -> ShortDeviceKind {
29        self.kind
30    }
31}
32
33/// A conservative lexical family for one short device declaration.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35#[non_exhaustive]
36pub enum ShortDeviceKind {
37    /// A dollar-bearing value whose effective spelling depends on interpolation.
38    Deferred,
39    /// A selector containing a non-empty CDI-like `vendor/device=name` split.
40    Cdi,
41    /// A slash-prefixed or colon-delimited path-like spelling.
42    Path,
43    /// Any other raw provider-dependent spelling.
44    Opaque,
45}
46
47/// One mapping-form service device declaration.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct LongDevice {
50    span: SourceSpan,
51    source: Option<Located<String>>,
52    target: Option<Located<String>>,
53    permissions: Option<Located<String>>,
54    extension_fields: Vec<FieldReference>,
55    unknown_fields: Vec<FieldReference>,
56}
57
58impl LongDevice {
59    pub(crate) const fn new(span: SourceSpan) -> Self {
60        Self {
61            span,
62            source: None,
63            target: None,
64            permissions: None,
65            extension_fields: Vec::new(),
66            unknown_fields: Vec::new(),
67        }
68    }
69
70    pub(crate) fn set_source(&mut self, source: Located<String>) {
71        self.source = Some(source);
72    }
73
74    pub(crate) fn set_target(&mut self, target: Located<String>) {
75        self.target = Some(target);
76    }
77
78    pub(crate) fn set_permissions(&mut self, permissions: Located<String>) {
79        self.permissions = Some(permissions);
80    }
81
82    pub(crate) fn push_extension(&mut self, field: FieldReference) {
83        self.extension_fields.push(field);
84    }
85
86    pub(crate) fn push_unknown(&mut self, field: FieldReference) {
87        self.unknown_fields.push(field);
88    }
89
90    /// Returns the complete mapping span.
91    #[must_use]
92    pub const fn span(&self) -> SourceSpan {
93        self.span
94    }
95
96    /// Returns the required source when it was valid and present.
97    #[must_use]
98    pub const fn source(&self) -> Option<&Located<String>> {
99        self.source.as_ref()
100    }
101
102    /// Returns the optional target without host/container path interpretation.
103    #[must_use]
104    pub const fn target(&self) -> Option<&Located<String>> {
105        self.target.as_ref()
106    }
107
108    /// Returns the optional raw permissions string without validating its letters or meaning.
109    #[must_use]
110    pub const fn permissions(&self) -> Option<&Located<String>> {
111        self.permissions.as_ref()
112    }
113
114    /// Returns retained `x-` options in authored order.
115    #[must_use]
116    pub fn extension_fields(&self) -> &[FieldReference] {
117        &self.extension_fields
118    }
119
120    /// Returns unrecognized mapping options in authored order.
121    #[must_use]
122    pub fn unknown_fields(&self) -> &[FieldReference] {
123        &self.unknown_fields
124    }
125}
126
127/// One ordered service device item with its authored syntax form retained.
128#[derive(Debug, Clone, PartialEq, Eq)]
129#[non_exhaustive]
130pub enum Device {
131    /// A raw scalar short form.
132    Short(ShortDevice),
133    /// A mapping long form.
134    Long(LongDevice),
135}
136
137impl Device {
138    /// Returns the complete item span.
139    #[must_use]
140    pub const fn span(&self) -> SourceSpan {
141        match self {
142            Self::Short(device) => device.raw().span(),
143            Self::Long(device) => device.span(),
144        }
145    }
146}
147
148/// An explicitly authored ordered service `devices` sequence.
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct Devices {
151    span: SourceSpan,
152    items: Vec<Device>,
153}
154
155impl Devices {
156    pub(crate) const fn new(span: SourceSpan, items: Vec<Device>) -> Self {
157        Self { span, items }
158    }
159
160    /// Returns the complete sequence span.
161    #[must_use]
162    pub const fn span(&self) -> SourceSpan {
163        self.span
164    }
165
166    /// Returns items in authored order, including exact duplicates.
167    #[must_use]
168    pub fn items(&self) -> &[Device] {
169        &self.items
170    }
171}
172
173fn classify_short_device(value: &str) -> ShortDeviceKind {
174    if value.contains('$') {
175        return ShortDeviceKind::Deferred;
176    }
177    if value
178        .split_once('=')
179        .is_some_and(|(selector, name)| !selector.is_empty() && !name.is_empty() && selector.contains('/'))
180    {
181        return ShortDeviceKind::Cdi;
182    }
183    if value.starts_with('/') || value.starts_with('.') || value.contains(':') || value.starts_with(r"\\") {
184        return ShortDeviceKind::Path;
185    }
186    ShortDeviceKind::Opaque
187}
188
189pub(crate) fn valid_generated_device_string(value: &str, require_non_empty: bool) -> bool {
190    (!require_non_empty || !value.is_empty()) && !value.contains(['\0', '\r', '\n', '$'])
191}
192
193#[cfg(test)]
194mod tests {
195    use super::{ShortDeviceKind, classify_short_device, valid_generated_device_string};
196
197    #[test]
198    fn classification_is_lexical_and_raw_preserving() {
199        assert_eq!(classify_short_device("/dev/dri:/dev/dri:rwm"), ShortDeviceKind::Path);
200        assert_eq!(classify_short_device("vendor.example/device=gpu"), ShortDeviceKind::Cdi);
201        assert_eq!(classify_short_device("${DEVICE}"), ShortDeviceKind::Deferred);
202        assert_eq!(classify_short_device("provider-token"), ShortDeviceKind::Opaque);
203    }
204
205    #[test]
206    fn generated_device_strings_only_enforce_safe_resolved_output() {
207        assert!(valid_generated_device_string("not-a-host-device", true));
208        assert!(valid_generated_device_string("not-permissions", false));
209        assert!(!valid_generated_device_string("", true));
210        assert!(!valid_generated_device_string("${DEVICE}", true));
211        assert!(!valid_generated_device_string("line\nbreak", false));
212    }
213}