1use super::{BooleanValue, ComposeScalar, FieldReference, Labels, 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 recursive: Option<Located<String>>,
186 extension_fields: Vec<FieldReference>,
187 unknown_fields: Vec<FieldReference>,
188}
189
190impl BindOptions {
191 pub(crate) fn new(span: SourceSpan) -> Self {
192 Self {
193 span,
194 propagation: None,
195 create_host_path: None,
196 selinux: None,
197 recursive: None,
198 extension_fields: Vec::new(),
199 unknown_fields: Vec::new(),
200 }
201 }
202
203 pub(crate) fn set_propagation(&mut self, value: Located<String>) {
204 self.propagation = Some(value);
205 }
206
207 pub(crate) fn set_create_host_path(&mut self, value: Located<BooleanValue>) {
208 self.create_host_path = Some(value);
209 }
210
211 pub(crate) fn set_selinux(&mut self, value: Located<SelinuxRelabel>) {
212 self.selinux = Some(value);
213 }
214
215 pub(crate) fn set_recursive(&mut self, value: Located<String>) {
216 self.recursive = Some(value);
217 }
218
219 pub(crate) fn push_extension(&mut self, field: FieldReference) {
220 self.extension_fields.push(field);
221 }
222
223 pub(crate) fn push_unknown(&mut self, field: FieldReference) {
224 self.unknown_fields.push(field);
225 }
226
227 #[must_use]
229 pub const fn span(&self) -> SourceSpan {
230 self.span
231 }
232
233 #[must_use]
235 pub const fn propagation(&self) -> Option<&Located<String>> {
236 self.propagation.as_ref()
237 }
238
239 #[must_use]
244 pub const fn create_host_path(&self) -> Option<&Located<BooleanValue>> {
245 self.create_host_path.as_ref()
246 }
247
248 #[must_use]
250 pub const fn selinux(&self) -> Option<&Located<SelinuxRelabel>> {
251 self.selinux.as_ref()
252 }
253
254 #[must_use]
256 pub const fn recursive(&self) -> Option<&Located<String>> {
257 self.recursive.as_ref()
258 }
259
260 #[must_use]
262 pub fn extension_fields(&self) -> &[FieldReference] {
263 &self.extension_fields
264 }
265
266 #[must_use]
268 pub fn unknown_fields(&self) -> &[FieldReference] {
269 &self.unknown_fields
270 }
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
275pub struct ImageMountOptions {
276 span: SourceSpan,
277 subpath: Option<Located<String>>,
278 extension_fields: Vec<FieldReference>,
279 unknown_fields: Vec<FieldReference>,
280}
281
282impl ImageMountOptions {
283 pub(crate) fn new(span: SourceSpan) -> Self {
284 Self {
285 span,
286 subpath: None,
287 extension_fields: Vec::new(),
288 unknown_fields: Vec::new(),
289 }
290 }
291 pub(crate) fn set_subpath(&mut self, value: Located<String>) {
292 self.subpath = Some(value);
293 }
294 pub(crate) fn push_extension(&mut self, field: FieldReference) {
295 self.extension_fields.push(field);
296 }
297 pub(crate) fn push_unknown(&mut self, field: FieldReference) {
298 self.unknown_fields.push(field);
299 }
300 #[must_use]
302 pub const fn span(&self) -> SourceSpan {
303 self.span
304 }
305 #[must_use]
307 pub const fn subpath(&self) -> Option<&Located<String>> {
308 self.subpath.as_ref()
309 }
310 #[must_use]
312 pub fn extension_fields(&self) -> &[FieldReference] {
313 &self.extension_fields
314 }
315 #[must_use]
317 pub fn unknown_fields(&self) -> &[FieldReference] {
318 &self.unknown_fields
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct TmpfsMountOptions {
325 span: SourceSpan,
326 size: Option<Located<ComposeScalar>>,
327 mode: Option<Located<ComposeScalar>>,
328 extension_fields: Vec<FieldReference>,
329 unknown_fields: Vec<FieldReference>,
330}
331
332impl TmpfsMountOptions {
333 pub(crate) fn new(span: SourceSpan) -> Self {
334 Self {
335 span,
336 size: None,
337 mode: None,
338 extension_fields: Vec::new(),
339 unknown_fields: Vec::new(),
340 }
341 }
342 pub(crate) fn set_size(&mut self, value: Located<ComposeScalar>) {
343 self.size = Some(value);
344 }
345 pub(crate) fn set_mode(&mut self, value: Located<ComposeScalar>) {
346 self.mode = Some(value);
347 }
348 pub(crate) fn push_extension(&mut self, field: FieldReference) {
349 self.extension_fields.push(field);
350 }
351 pub(crate) fn push_unknown(&mut self, field: FieldReference) {
352 self.unknown_fields.push(field);
353 }
354 #[must_use]
356 pub const fn span(&self) -> SourceSpan {
357 self.span
358 }
359 #[must_use]
361 pub const fn size(&self) -> Option<&Located<ComposeScalar>> {
362 self.size.as_ref()
363 }
364 #[must_use]
366 pub const fn mode(&self) -> Option<&Located<ComposeScalar>> {
367 self.mode.as_ref()
368 }
369 #[must_use]
371 pub fn extension_fields(&self) -> &[FieldReference] {
372 &self.extension_fields
373 }
374 #[must_use]
376 pub fn unknown_fields(&self) -> &[FieldReference] {
377 &self.unknown_fields
378 }
379}
380
381#[derive(Debug, Clone, PartialEq, Eq)]
383pub struct VolumeMountOptions {
384 span: SourceSpan,
385 nocopy: Option<Located<BooleanValue>>,
386 subpath: Option<Located<String>>,
387 labels: Option<Labels>,
388 extension_fields: Vec<FieldReference>,
389 unknown_fields: Vec<FieldReference>,
390}
391
392impl VolumeMountOptions {
393 pub(crate) fn new(span: SourceSpan) -> Self {
394 Self {
395 span,
396 nocopy: None,
397 subpath: None,
398 labels: None,
399 extension_fields: Vec::new(),
400 unknown_fields: Vec::new(),
401 }
402 }
403 pub(crate) fn set_nocopy(&mut self, value: Located<BooleanValue>) {
404 self.nocopy = Some(value);
405 }
406 pub(crate) fn set_subpath(&mut self, value: Located<String>) {
407 self.subpath = Some(value);
408 }
409 pub(crate) fn set_labels(&mut self, value: Labels) {
410 self.labels = Some(value);
411 }
412 pub(crate) fn push_extension(&mut self, field: FieldReference) {
413 self.extension_fields.push(field);
414 }
415 pub(crate) fn push_unknown(&mut self, field: FieldReference) {
416 self.unknown_fields.push(field);
417 }
418 #[must_use]
420 pub const fn span(&self) -> SourceSpan {
421 self.span
422 }
423 #[must_use]
425 pub const fn nocopy(&self) -> Option<&Located<BooleanValue>> {
426 self.nocopy.as_ref()
427 }
428 #[must_use]
430 pub const fn subpath(&self) -> Option<&Located<String>> {
431 self.subpath.as_ref()
432 }
433 #[must_use]
435 pub const fn labels(&self) -> Option<&Labels> {
436 self.labels.as_ref()
437 }
438 #[must_use]
440 pub fn extension_fields(&self) -> &[FieldReference] {
441 &self.extension_fields
442 }
443 #[must_use]
445 pub fn unknown_fields(&self) -> &[FieldReference] {
446 &self.unknown_fields
447 }
448}
449
450#[derive(Debug, Clone, PartialEq, Eq)]
452pub struct LongVolumeMount {
453 span: SourceSpan,
454 mount_type: Option<Located<MountType>>,
455 source: Option<Located<String>>,
456 target: Option<Located<String>>,
457 target_path: Option<Located<ContainerPath>>,
458 read_only: Option<Located<BooleanValue>>,
459 consistency: Option<Located<String>>,
460 bind: Option<BindOptions>,
461 image: Option<ImageMountOptions>,
462 tmpfs: Option<TmpfsMountOptions>,
463 volume: Option<VolumeMountOptions>,
464 extension_fields: Vec<FieldReference>,
465 unknown_fields: Vec<FieldReference>,
466}
467
468impl LongVolumeMount {
469 pub(crate) fn new(span: SourceSpan) -> Self {
470 Self {
471 span,
472 mount_type: None,
473 source: None,
474 target: None,
475 target_path: None,
476 read_only: None,
477 consistency: None,
478 bind: None,
479 image: None,
480 tmpfs: None,
481 volume: None,
482 extension_fields: Vec::new(),
483 unknown_fields: Vec::new(),
484 }
485 }
486
487 pub(crate) fn set_mount_type(&mut self, value: Located<MountType>) {
488 self.mount_type = Some(value);
489 }
490
491 pub(crate) fn set_source(&mut self, value: Located<String>) {
492 self.source = Some(value);
493 }
494
495 pub(crate) fn set_target(&mut self, value: Located<String>) {
496 self.target_path = Some(Located::new(ContainerPath::parse(value.value.clone()), value.span));
497 self.target = Some(value);
498 }
499
500 pub(crate) fn set_read_only(&mut self, value: Located<BooleanValue>) {
501 self.read_only = Some(value);
502 }
503
504 pub(crate) fn set_consistency(&mut self, value: Located<String>) {
505 self.consistency = Some(value);
506 }
507
508 pub(crate) fn set_bind(&mut self, value: BindOptions) {
509 self.bind = Some(value);
510 }
511 pub(crate) fn set_image(&mut self, value: ImageMountOptions) {
512 self.image = Some(value);
513 }
514 pub(crate) fn set_tmpfs(&mut self, value: TmpfsMountOptions) {
515 self.tmpfs = Some(value);
516 }
517 pub(crate) fn set_volume(&mut self, value: VolumeMountOptions) {
518 self.volume = Some(value);
519 }
520
521 pub(crate) fn push_extension(&mut self, field: FieldReference) {
522 self.extension_fields.push(field);
523 }
524
525 pub(crate) fn push_unknown(&mut self, field: FieldReference) {
526 self.unknown_fields.push(field);
527 }
528
529 #[must_use]
531 pub const fn span(&self) -> SourceSpan {
532 self.span
533 }
534
535 #[must_use]
537 pub const fn mount_type(&self) -> Option<&Located<MountType>> {
538 self.mount_type.as_ref()
539 }
540
541 #[must_use]
543 pub const fn source(&self) -> Option<&Located<String>> {
544 self.source.as_ref()
545 }
546
547 #[must_use]
549 pub const fn target(&self) -> Option<&Located<String>> {
550 self.target.as_ref()
551 }
552
553 #[must_use]
555 pub const fn target_path(&self) -> Option<&Located<ContainerPath>> {
556 self.target_path.as_ref()
557 }
558
559 #[must_use]
561 pub const fn read_only(&self) -> Option<&Located<BooleanValue>> {
562 self.read_only.as_ref()
563 }
564
565 #[must_use]
567 pub const fn consistency(&self) -> Option<&Located<String>> {
568 self.consistency.as_ref()
569 }
570
571 #[must_use]
573 pub const fn bind(&self) -> Option<&BindOptions> {
574 self.bind.as_ref()
575 }
576
577 #[must_use]
579 pub const fn image(&self) -> Option<&ImageMountOptions> {
580 self.image.as_ref()
581 }
582 #[must_use]
584 pub const fn tmpfs(&self) -> Option<&TmpfsMountOptions> {
585 self.tmpfs.as_ref()
586 }
587 #[must_use]
589 pub const fn volume(&self) -> Option<&VolumeMountOptions> {
590 self.volume.as_ref()
591 }
592
593 #[must_use]
595 pub fn extension_fields(&self) -> &[FieldReference] {
596 &self.extension_fields
597 }
598
599 #[must_use]
601 pub fn unknown_fields(&self) -> &[FieldReference] {
602 &self.unknown_fields
603 }
604}
605
606#[derive(Debug, Clone, PartialEq, Eq)]
608pub enum VolumeMount {
609 Short(ShortVolumeMount),
611 Long(Box<LongVolumeMount>),
613}
614
615impl VolumeMount {
616 #[must_use]
618 pub const fn syntax(&self) -> VolumeSyntax {
619 match self {
620 Self::Short(_) => VolumeSyntax::Short,
621 Self::Long(_) => VolumeSyntax::Long,
622 }
623 }
624
625 #[must_use]
627 pub const fn span(&self) -> SourceSpan {
628 match self {
629 Self::Short(value) => value.raw().span(),
630 Self::Long(value) => value.span(),
631 }
632 }
633
634 #[must_use]
636 pub fn selinux_relabel(&self) -> Option<SelinuxRelabel> {
637 match self {
638 Self::Short(value) => value.selinux_relabel(),
639 Self::Long(value) => value.bind()?.selinux().map(|mode| *mode.value()),
640 }
641 }
642}
643
644fn split_short_volume(value: &str) -> (Option<String>, Option<String>, Vec<String>) {
645 let fields = split_colon_fields(value);
646 match fields.as_slice() {
647 [] => (None, None, Vec::new()),
648 [target] => (None, Some((*target).to_owned()), Vec::new()),
649 [source, target] => (Some((*source).to_owned()), Some((*target).to_owned()), Vec::new()),
650 [source, middle @ .., options] => (
651 Some((*source).to_owned()),
652 Some(middle.join(":")),
653 options.split(',').map(str::to_owned).collect(),
654 ),
655 }
656}
657
658fn split_colon_fields(value: &str) -> Vec<&str> {
659 let mut fields = Vec::new();
660 let mut start = 0;
661 for (index, character) in value.char_indices() {
662 if character != ':' || is_drive_separator(value, start, index) {
663 continue;
664 }
665 fields.push(&value[start..index]);
666 start = index + character.len_utf8();
667 }
668 fields.push(&value[start..]);
669 fields
670}
671
672fn is_drive_separator(value: &str, field_start: usize, colon_index: usize) -> bool {
673 let field = &value[field_start..colon_index];
674 let next = value[colon_index + 1..].chars().next();
675 field.len() == 1 && field.as_bytes()[0].is_ascii_alphabetic() && matches!(next, Some('/' | '\\'))
676}
677
678fn is_windows_drive_absolute(value: &str) -> bool {
679 value.as_bytes().get(1) == Some(&b':')
680 && value.as_bytes().first().is_some_and(u8::is_ascii_alphabetic)
681 && matches!(value.as_bytes().get(2), Some(b'/' | b'\\'))
682}
683
684#[cfg(test)]
685mod tests {
686 use super::{ContainerPathKind, ShortVolumeMount, split_short_volume};
687 use crate::model::Located;
688 use crate::source::{SourceId, SourceSpan};
689
690 #[test]
691 fn conservatively_splits_linux_and_windows_short_mounts() {
692 assert_eq!(
693 split_short_volume("./data:/var/lib/data:Z,ro"),
694 (
695 Some("./data".to_owned()),
696 Some("/var/lib/data".to_owned()),
697 vec!["Z".to_owned(), "ro".to_owned()]
698 )
699 );
700 assert_eq!(
701 split_short_volume(r"C:\data:/var/lib/data:z"),
702 (
703 Some(r"C:\data".to_owned()),
704 Some("/var/lib/data".to_owned()),
705 vec!["z".to_owned()]
706 )
707 );
708 assert_eq!(
709 split_short_volume("cache:/cache"),
710 (Some("cache".to_owned()), Some("/cache".to_owned()), Vec::new())
711 );
712 }
713
714 #[test]
715 fn classifies_anonymous_targets_without_host_path_apis() -> Result<(), &'static str> {
716 let raw = "/project/node_modules";
717 let span = SourceSpan::new(SourceId::new(1), 0, raw.len()).ok_or("valid test span expected")?;
718 let mount = ShortVolumeMount::new(Located::new(raw.to_owned(), span));
719 assert_eq!(mount.source(), None);
720 assert_eq!(
721 mount.target_path().map(super::ContainerPath::kind),
722 Some(ContainerPathKind::UnixAbsolute)
723 );
724 Ok(())
725 }
726}