compose_lens/model/
device.rs1use crate::source::SourceSpan;
4
5use super::{FieldReference, Located};
6
7#[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 #[must_use]
22 pub const fn raw(&self) -> &Located<String> {
23 &self.raw
24 }
25
26 #[must_use]
28 pub const fn kind(&self) -> ShortDeviceKind {
29 self.kind
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35#[non_exhaustive]
36pub enum ShortDeviceKind {
37 Deferred,
39 Cdi,
41 Path,
43 Opaque,
45}
46
47#[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 #[must_use]
92 pub const fn span(&self) -> SourceSpan {
93 self.span
94 }
95
96 #[must_use]
98 pub const fn source(&self) -> Option<&Located<String>> {
99 self.source.as_ref()
100 }
101
102 #[must_use]
104 pub const fn target(&self) -> Option<&Located<String>> {
105 self.target.as_ref()
106 }
107
108 #[must_use]
110 pub const fn permissions(&self) -> Option<&Located<String>> {
111 self.permissions.as_ref()
112 }
113
114 #[must_use]
116 pub fn extension_fields(&self) -> &[FieldReference] {
117 &self.extension_fields
118 }
119
120 #[must_use]
122 pub fn unknown_fields(&self) -> &[FieldReference] {
123 &self.unknown_fields
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
129#[non_exhaustive]
130pub enum Device {
131 Short(ShortDevice),
133 Long(LongDevice),
135}
136
137impl Device {
138 #[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#[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 #[must_use]
162 pub const fn span(&self) -> SourceSpan {
163 self.span
164 }
165
166 #[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}