1use super::{BooleanValue, FieldReference, Located};
4use crate::source::SourceSpan;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum VolumeSyntax {
9 Short,
11 Long,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum SelinuxRelabel {
18 Shared,
20 Private,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct ContainerPath {
27 raw: String,
28 kind: ContainerPathKind,
29}
30
31impl ContainerPath {
32 fn parse(raw: String) -> Self {
33 let kind = if raw.starts_with('/') {
34 ContainerPathKind::UnixAbsolute
35 } else if is_windows_drive_absolute(&raw) {
36 ContainerPathKind::WindowsDriveAbsolute
37 } else if raw.starts_with(r"\\") || raw.starts_with("//") {
38 ContainerPathKind::WindowsUnc
39 } else if raw.contains('$') {
40 ContainerPathKind::Deferred
41 } else {
42 ContainerPathKind::Relative
43 };
44 Self { raw, kind }
45 }
46
47 #[must_use]
49 pub fn raw(&self) -> &str {
50 &self.raw
51 }
52
53 #[must_use]
55 pub const fn kind(&self) -> ContainerPathKind {
56 self.kind
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum ContainerPathKind {
63 UnixAbsolute,
65 WindowsDriveAbsolute,
67 WindowsUnc,
69 Relative,
71 Deferred,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Hash)]
77pub enum MountType {
78 Volume,
80 Bind,
82 Tmpfs,
84 NamedPipe,
86 Image,
88 Cluster,
90 Other(String),
92}
93
94impl MountType {
95 pub(crate) fn from_text(value: String) -> Self {
96 match value.as_str() {
97 "volume" => Self::Volume,
98 "bind" => Self::Bind,
99 "tmpfs" => Self::Tmpfs,
100 "npipe" => Self::NamedPipe,
101 "image" => Self::Image,
102 "cluster" => Self::Cluster,
103 _ => Self::Other(value),
104 }
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct ShortVolumeMount {
115 raw: Located<String>,
116 source: Option<String>,
117 target: Option<String>,
118 target_path: Option<ContainerPath>,
119 options: Vec<String>,
120}
121
122impl ShortVolumeMount {
123 pub(crate) fn new(raw: Located<String>) -> Self {
124 let (source, target, options) = split_short_volume(raw.value());
125 Self {
126 raw,
127 source,
128 target_path: target.as_ref().map(|value| ContainerPath::parse(value.clone())),
129 target,
130 options,
131 }
132 }
133
134 #[must_use]
136 pub const fn raw(&self) -> &Located<String> {
137 &self.raw
138 }
139
140 #[must_use]
142 pub fn source(&self) -> Option<&str> {
143 self.source.as_deref()
144 }
145
146 #[must_use]
148 pub fn target(&self) -> Option<&str> {
149 self.target.as_deref()
150 }
151
152 #[must_use]
157 pub const fn target_path(&self) -> Option<&ContainerPath> {
158 self.target_path.as_ref()
159 }
160
161 #[must_use]
163 pub fn options(&self) -> &[String] {
164 &self.options
165 }
166
167 #[must_use]
169 pub fn selinux_relabel(&self) -> Option<SelinuxRelabel> {
170 self.options.iter().find_map(|option| match option.as_str() {
171 "z" => Some(SelinuxRelabel::Shared),
172 "Z" => Some(SelinuxRelabel::Private),
173 _ => None,
174 })
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct BindOptions {
181 span: SourceSpan,
182 propagation: Option<Located<String>>,
183 create_host_path: Option<Located<BooleanValue>>,
184 selinux: Option<Located<SelinuxRelabel>>,
185 extension_fields: Vec<FieldReference>,
186 unknown_fields: Vec<FieldReference>,
187}
188
189impl BindOptions {
190 pub(crate) fn new(span: SourceSpan) -> Self {
191 Self {
192 span,
193 propagation: None,
194 create_host_path: None,
195 selinux: None,
196 extension_fields: Vec::new(),
197 unknown_fields: Vec::new(),
198 }
199 }
200
201 pub(crate) fn set_propagation(&mut self, value: Located<String>) {
202 self.propagation = Some(value);
203 }
204
205 pub(crate) fn set_create_host_path(&mut self, value: Located<BooleanValue>) {
206 self.create_host_path = Some(value);
207 }
208
209 pub(crate) fn set_selinux(&mut self, value: Located<SelinuxRelabel>) {
210 self.selinux = Some(value);
211 }
212
213 pub(super) fn push_extension(&mut self, field: FieldReference) {
214 self.extension_fields.push(field);
215 }
216
217 pub(super) fn push_unknown(&mut self, field: FieldReference) {
218 self.unknown_fields.push(field);
219 }
220
221 #[must_use]
223 pub const fn span(&self) -> SourceSpan {
224 self.span
225 }
226
227 #[must_use]
229 pub const fn propagation(&self) -> Option<&Located<String>> {
230 self.propagation.as_ref()
231 }
232
233 #[must_use]
238 pub const fn create_host_path(&self) -> Option<&Located<BooleanValue>> {
239 self.create_host_path.as_ref()
240 }
241
242 #[must_use]
244 pub const fn selinux(&self) -> Option<&Located<SelinuxRelabel>> {
245 self.selinux.as_ref()
246 }
247
248 #[must_use]
250 pub fn extension_fields(&self) -> &[FieldReference] {
251 &self.extension_fields
252 }
253
254 #[must_use]
256 pub fn unknown_fields(&self) -> &[FieldReference] {
257 &self.unknown_fields
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct LongVolumeMount {
264 span: SourceSpan,
265 mount_type: Option<Located<MountType>>,
266 source: Option<Located<String>>,
267 target: Option<Located<String>>,
268 target_path: Option<Located<ContainerPath>>,
269 read_only: Option<Located<BooleanValue>>,
270 bind: Option<BindOptions>,
271 extension_fields: Vec<FieldReference>,
272 unknown_fields: Vec<FieldReference>,
273}
274
275impl LongVolumeMount {
276 pub(crate) fn new(span: SourceSpan) -> Self {
277 Self {
278 span,
279 mount_type: None,
280 source: None,
281 target: None,
282 target_path: None,
283 read_only: None,
284 bind: None,
285 extension_fields: Vec::new(),
286 unknown_fields: Vec::new(),
287 }
288 }
289
290 pub(crate) fn set_mount_type(&mut self, value: Located<MountType>) {
291 self.mount_type = Some(value);
292 }
293
294 pub(crate) fn set_source(&mut self, value: Located<String>) {
295 self.source = Some(value);
296 }
297
298 pub(crate) fn set_target(&mut self, value: Located<String>) {
299 self.target_path = Some(Located::new(ContainerPath::parse(value.value.clone()), value.span));
300 self.target = Some(value);
301 }
302
303 pub(crate) fn set_read_only(&mut self, value: Located<BooleanValue>) {
304 self.read_only = Some(value);
305 }
306
307 pub(crate) fn set_bind(&mut self, value: BindOptions) {
308 self.bind = Some(value);
309 }
310
311 pub(super) fn push_extension(&mut self, field: FieldReference) {
312 self.extension_fields.push(field);
313 }
314
315 pub(super) fn push_unknown(&mut self, field: FieldReference) {
316 self.unknown_fields.push(field);
317 }
318
319 #[must_use]
321 pub const fn span(&self) -> SourceSpan {
322 self.span
323 }
324
325 #[must_use]
327 pub const fn mount_type(&self) -> Option<&Located<MountType>> {
328 self.mount_type.as_ref()
329 }
330
331 #[must_use]
333 pub const fn source(&self) -> Option<&Located<String>> {
334 self.source.as_ref()
335 }
336
337 #[must_use]
339 pub const fn target(&self) -> Option<&Located<String>> {
340 self.target.as_ref()
341 }
342
343 #[must_use]
345 pub const fn target_path(&self) -> Option<&Located<ContainerPath>> {
346 self.target_path.as_ref()
347 }
348
349 #[must_use]
351 pub const fn read_only(&self) -> Option<&Located<BooleanValue>> {
352 self.read_only.as_ref()
353 }
354
355 #[must_use]
357 pub const fn bind(&self) -> Option<&BindOptions> {
358 self.bind.as_ref()
359 }
360
361 #[must_use]
363 pub fn extension_fields(&self) -> &[FieldReference] {
364 &self.extension_fields
365 }
366
367 #[must_use]
369 pub fn unknown_fields(&self) -> &[FieldReference] {
370 &self.unknown_fields
371 }
372}
373
374#[derive(Debug, Clone, PartialEq, Eq)]
376pub enum VolumeMount {
377 Short(ShortVolumeMount),
379 Long(Box<LongVolumeMount>),
381}
382
383impl VolumeMount {
384 #[must_use]
386 pub const fn syntax(&self) -> VolumeSyntax {
387 match self {
388 Self::Short(_) => VolumeSyntax::Short,
389 Self::Long(_) => VolumeSyntax::Long,
390 }
391 }
392
393 #[must_use]
395 pub const fn span(&self) -> SourceSpan {
396 match self {
397 Self::Short(value) => value.raw().span(),
398 Self::Long(value) => value.span(),
399 }
400 }
401
402 #[must_use]
404 pub fn selinux_relabel(&self) -> Option<SelinuxRelabel> {
405 match self {
406 Self::Short(value) => value.selinux_relabel(),
407 Self::Long(value) => value.bind()?.selinux().map(|mode| *mode.value()),
408 }
409 }
410}
411
412fn split_short_volume(value: &str) -> (Option<String>, Option<String>, Vec<String>) {
413 let fields = split_colon_fields(value);
414 match fields.as_slice() {
415 [] => (None, None, Vec::new()),
416 [target] => (None, Some((*target).to_owned()), Vec::new()),
417 [source, target] => (Some((*source).to_owned()), Some((*target).to_owned()), Vec::new()),
418 [source, middle @ .., options] => (
419 Some((*source).to_owned()),
420 Some(middle.join(":")),
421 options.split(',').map(str::to_owned).collect(),
422 ),
423 }
424}
425
426fn split_colon_fields(value: &str) -> Vec<&str> {
427 let mut fields = Vec::new();
428 let mut start = 0;
429 for (index, character) in value.char_indices() {
430 if character != ':' || is_drive_separator(value, start, index) {
431 continue;
432 }
433 fields.push(&value[start..index]);
434 start = index + character.len_utf8();
435 }
436 fields.push(&value[start..]);
437 fields
438}
439
440fn is_drive_separator(value: &str, field_start: usize, colon_index: usize) -> bool {
441 let field = &value[field_start..colon_index];
442 let next = value[colon_index + 1..].chars().next();
443 field.len() == 1 && field.as_bytes()[0].is_ascii_alphabetic() && matches!(next, Some('/' | '\\'))
444}
445
446fn is_windows_drive_absolute(value: &str) -> bool {
447 value.as_bytes().get(1) == Some(&b':')
448 && value.as_bytes().first().is_some_and(u8::is_ascii_alphabetic)
449 && matches!(value.as_bytes().get(2), Some(b'/' | b'\\'))
450}
451
452#[cfg(test)]
453mod tests {
454 use super::{ContainerPathKind, ShortVolumeMount, split_short_volume};
455 use crate::model::Located;
456 use crate::source::{SourceId, SourceSpan};
457
458 #[test]
459 fn conservatively_splits_linux_and_windows_short_mounts() {
460 assert_eq!(
461 split_short_volume("./data:/var/lib/data:Z,ro"),
462 (
463 Some("./data".to_owned()),
464 Some("/var/lib/data".to_owned()),
465 vec!["Z".to_owned(), "ro".to_owned()]
466 )
467 );
468 assert_eq!(
469 split_short_volume(r"C:\data:/var/lib/data:z"),
470 (
471 Some(r"C:\data".to_owned()),
472 Some("/var/lib/data".to_owned()),
473 vec!["z".to_owned()]
474 )
475 );
476 assert_eq!(
477 split_short_volume("cache:/cache"),
478 (Some("cache".to_owned()), Some("/cache".to_owned()), Vec::new())
479 );
480 }
481
482 #[test]
483 fn classifies_anonymous_targets_without_host_path_apis() -> Result<(), &'static str> {
484 let raw = "/project/node_modules";
485 let span = SourceSpan::new(SourceId::new(1), 0, raw.len()).ok_or("valid test span expected")?;
486 let mount = ShortVolumeMount::new(Located::new(raw.to_owned(), span));
487 assert_eq!(mount.source(), None);
488 assert_eq!(
489 mount.target_path().map(super::ContainerPath::kind),
490 Some(ContainerPathKind::UnixAbsolute)
491 );
492 Ok(())
493 }
494}