1use std::fmt;
4
5use crate::{
6 DeploymentResourceId, Diagnostic, DiagnosticCode, PodmanLensResult, ResourceKind, SensitiveInputReference,
7};
8
9const MAX_ARGUMENTS: usize = 128;
10const MAX_ARGUMENT_BYTES: usize = 4096;
11const MAX_VALUE_BYTES: usize = 4096;
12const MAX_LABELS: usize = 128;
13const MAX_ENVIRONMENT: usize = 128;
14const MAX_PATH_BYTES: usize = 4096;
15
16#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct ArgumentArray(Vec<String>);
19
20impl ArgumentArray {
21 pub fn new<I, S>(arguments: I) -> PodmanLensResult<Self>
28 where
29 I: IntoIterator<Item = S>,
30 S: Into<String>,
31 {
32 let arguments = arguments.into_iter().map(Into::into).collect::<Vec<String>>();
33 if arguments.is_empty()
34 || arguments.len() > MAX_ARGUMENTS
35 || arguments
36 .iter()
37 .any(|argument| !valid_non_control(argument, MAX_ARGUMENT_BYTES))
38 {
39 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
40 }
41 Ok(Self(arguments))
42 }
43
44 #[must_use]
46 pub fn values(&self) -> &[String] {
47 &self.0
48 }
49}
50
51#[derive(Clone, Debug, Eq, PartialEq)]
53pub struct ContainerUser(String);
54
55impl ContainerUser {
56 pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
64 let value = value.into();
65 let mut components = value.split(':');
66 let Some(user) = components.next() else {
67 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
68 };
69 let group = components.next();
70 if value.len() > MAX_VALUE_BYTES
71 || !valid_user_component(user)
72 || group.is_some_and(|component| !valid_user_component(component))
73 || components.next().is_some()
74 {
75 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
76 }
77 Ok(Self(value))
78 }
79
80 #[must_use]
82 pub fn as_str(&self) -> &str {
83 &self.0
84 }
85}
86
87#[derive(Clone, Debug, Eq, PartialEq)]
89pub struct AbsoluteContainerPath(String);
90
91impl AbsoluteContainerPath {
92 pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
99 let value = value.into();
100 if !is_absolute_normalized_path(&value) {
101 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
102 }
103 Ok(Self(value))
104 }
105
106 #[must_use]
108 pub fn as_str(&self) -> &str {
109 &self.0
110 }
111}
112
113#[derive(Clone, Debug, Eq, PartialEq)]
115pub struct ContainerWorkdir(AbsoluteContainerPath);
116
117impl ContainerWorkdir {
118 #[must_use]
120 pub const fn new(path: AbsoluteContainerPath) -> Self {
121 Self(path)
122 }
123
124 #[must_use]
126 pub fn path(&self) -> &AbsoluteContainerPath {
127 &self.0
128 }
129}
130
131#[derive(Clone, Debug, Eq, PartialEq)]
133pub struct ContainerHostname(String);
134
135impl ContainerHostname {
136 pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
142 let value = value.into();
143 if value.is_empty()
144 || value.len() > 253
145 || value.split('.').any(|label| {
146 label.is_empty()
147 || label.len() > 63
148 || label.starts_with('-')
149 || label.ends_with('-')
150 || !label.bytes().all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
151 })
152 {
153 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
154 }
155 Ok(Self(value))
156 }
157
158 #[must_use]
160 pub fn as_str(&self) -> &str {
161 &self.0
162 }
163}
164
165#[derive(Clone, Debug, Eq, PartialEq)]
167pub struct LabelKey(String);
168
169impl LabelKey {
170 pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
176 let value = value.into();
177 if value.is_empty() || !valid_non_control(&value, MAX_VALUE_BYTES) || value.contains('=') {
178 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
179 }
180 Ok(Self(value))
181 }
182
183 #[must_use]
185 pub fn as_str(&self) -> &str {
186 &self.0
187 }
188}
189
190#[derive(Clone, Debug, Eq, PartialEq)]
196pub struct PublicLabelValue(String);
197
198impl PublicLabelValue {
199 pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
205 let value = value.into();
206 if !valid_value(&value) {
207 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
208 }
209 Ok(Self(value))
210 }
211
212 #[must_use]
214 pub fn as_str(&self) -> &str {
215 &self.0
216 }
217}
218
219#[derive(Clone, Debug, Eq, PartialEq)]
221pub struct Label {
222 key: LabelKey,
223 value: PublicLabelValue,
224}
225
226impl Label {
227 #[must_use]
229 pub const fn new(key: LabelKey, value: PublicLabelValue) -> Self {
230 Self { key, value }
231 }
232
233 #[must_use]
235 pub fn key(&self) -> &LabelKey {
236 &self.key
237 }
238
239 #[must_use]
241 pub fn value(&self) -> &PublicLabelValue {
242 &self.value
243 }
244}
245
246#[derive(Clone, Debug, Eq, PartialEq)]
248pub struct EnvironmentName(String);
249
250impl EnvironmentName {
251 pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
257 let value = value.into();
258 let mut bytes = value.bytes();
259 let Some(first) = bytes.next() else {
260 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
261 };
262 if value.len() > 256
263 || !(first.is_ascii_alphabetic() || first == b'_')
264 || !bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
265 {
266 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
267 }
268 Ok(Self(value))
269 }
270
271 #[must_use]
273 pub fn as_str(&self) -> &str {
274 &self.0
275 }
276}
277
278#[derive(Clone, Debug, Eq, PartialEq)]
284pub struct PublicEnvironmentValue(String);
285
286impl PublicEnvironmentValue {
287 pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
293 let value = value.into();
294 if !valid_value(&value) {
295 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
296 }
297 Ok(Self(value))
298 }
299
300 #[must_use]
302 pub fn as_str(&self) -> &str {
303 &self.0
304 }
305}
306
307#[derive(Clone, Eq, PartialEq)]
309pub struct SensitiveInlineEnvironmentValue(String);
310
311impl SensitiveInlineEnvironmentValue {
312 pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
318 let value = value.into();
319 if !valid_value(&value) {
320 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
321 }
322 Ok(Self(value))
323 }
324}
325
326impl fmt::Debug for SensitiveInlineEnvironmentValue {
327 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
328 formatter.write_str("SensitiveInlineEnvironmentValue([redacted])")
329 }
330}
331
332#[derive(Clone, Debug, Eq, PartialEq)]
334#[non_exhaustive]
335pub enum DeploymentEnvironmentValue {
336 Public(PublicEnvironmentValue),
338 SensitiveInline(SensitiveInlineEnvironmentValue),
340 External(SensitiveInputReference),
342}
343
344#[derive(Clone, Debug, Eq, PartialEq)]
346pub struct EnvironmentAssignment {
347 name: EnvironmentName,
348 value: DeploymentEnvironmentValue,
349}
350
351impl EnvironmentAssignment {
352 #[must_use]
354 pub const fn new(name: EnvironmentName, value: DeploymentEnvironmentValue) -> Self {
355 Self { name, value }
356 }
357
358 #[must_use]
360 pub fn name(&self) -> &EnvironmentName {
361 &self.name
362 }
363
364 #[must_use]
366 pub fn value(&self) -> &DeploymentEnvironmentValue {
367 &self.value
368 }
369}
370
371#[derive(Clone, Copy, Debug, Eq, PartialEq)]
373#[non_exhaustive]
374pub enum RestartPolicy {
375 No,
377 OnFailure,
379 Always,
381 UnlessStopped,
383}
384
385#[derive(Clone, Copy, Debug, Eq, PartialEq)]
387#[non_exhaustive]
388pub enum NamedVolumeCopyMode {
389 Copy,
391 NoCopy,
393}
394
395#[derive(Clone, Copy, Debug, Eq, PartialEq)]
397#[non_exhaustive]
398pub enum MountAccess {
399 ReadWrite,
401 ReadOnly,
403}
404
405impl MountAccess {
406 #[must_use]
408 pub const fn is_read_only(self) -> bool {
409 matches!(self, Self::ReadOnly)
410 }
411}
412
413#[derive(Clone, Debug, Eq, PartialEq)]
419pub struct VolumeSubpath(String);
420
421impl VolumeSubpath {
422 pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
428 let value = value.into();
429 if value.len() > MAX_PATH_BYTES
430 || !value.starts_with('/')
431 || value.contains('\\')
432 || value.chars().any(char::is_control)
433 || value
434 .split('/')
435 .skip(1)
436 .any(|component| component.is_empty() || matches!(component, "." | ".."))
437 {
438 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
439 }
440 Ok(Self(value))
441 }
442
443 #[must_use]
445 pub fn as_str(&self) -> &str {
446 &self.0
447 }
448}
449
450#[derive(Clone, Debug, Eq, PartialEq)]
452pub struct NamedVolumeMount {
453 source: DeploymentResourceId,
454 destination: AbsoluteContainerPath,
455 access: MountAccess,
456 copy_mode: NamedVolumeCopyMode,
457 subpath: Option<VolumeSubpath>,
458}
459
460impl NamedVolumeMount {
461 pub fn new(
467 source: DeploymentResourceId,
468 destination: AbsoluteContainerPath,
469 access: MountAccess,
470 copy_mode: NamedVolumeCopyMode,
471 ) -> PodmanLensResult<Self> {
472 if source.kind() != ResourceKind::Volume {
473 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
474 }
475 Ok(Self {
476 source,
477 destination,
478 access,
479 copy_mode,
480 subpath: None,
481 })
482 }
483
484 pub fn set_subpath(&mut self, subpath: VolumeSubpath) -> PodmanLensResult<()> {
493 if self.subpath.is_some() || self.copy_mode == NamedVolumeCopyMode::NoCopy {
494 return Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination));
495 }
496 self.subpath = Some(subpath);
497 Ok(())
498 }
499
500 #[must_use]
502 pub fn source(&self) -> &DeploymentResourceId {
503 &self.source
504 }
505
506 #[must_use]
508 pub fn destination(&self) -> &AbsoluteContainerPath {
509 &self.destination
510 }
511
512 #[must_use]
514 pub const fn is_read_only(&self) -> bool {
515 self.access.is_read_only()
516 }
517
518 #[must_use]
520 pub const fn access(&self) -> MountAccess {
521 self.access
522 }
523
524 #[must_use]
526 pub const fn copy_mode(&self) -> NamedVolumeCopyMode {
527 self.copy_mode
528 }
529
530 #[must_use]
532 pub fn subpath(&self) -> Option<&VolumeSubpath> {
533 self.subpath.as_ref()
534 }
535}
536
537#[derive(Clone, Debug, Eq, PartialEq)]
539pub struct BindMount {
540 source: AbsoluteContainerPath,
541 destination: AbsoluteContainerPath,
542 access: MountAccess,
543}
544
545impl BindMount {
546 #[must_use]
548 pub const fn new(source: AbsoluteContainerPath, destination: AbsoluteContainerPath, access: MountAccess) -> Self {
549 Self {
550 source,
551 destination,
552 access,
553 }
554 }
555
556 #[must_use]
558 pub fn source(&self) -> &AbsoluteContainerPath {
559 &self.source
560 }
561
562 #[must_use]
564 pub fn destination(&self) -> &AbsoluteContainerPath {
565 &self.destination
566 }
567
568 #[must_use]
570 pub const fn access(&self) -> MountAccess {
571 self.access
572 }
573}
574
575#[derive(Clone, Debug, Eq, PartialEq)]
577pub struct TmpfsMount {
578 destination: AbsoluteContainerPath,
579 access: MountAccess,
580}
581
582impl TmpfsMount {
583 #[must_use]
585 pub const fn new(destination: AbsoluteContainerPath, access: MountAccess) -> Self {
586 Self { destination, access }
587 }
588
589 #[must_use]
591 pub fn destination(&self) -> &AbsoluteContainerPath {
592 &self.destination
593 }
594
595 #[must_use]
597 pub const fn access(&self) -> MountAccess {
598 self.access
599 }
600}
601
602#[derive(Clone, Debug, Eq, PartialEq)]
604#[non_exhaustive]
605pub enum MountIntent {
606 NamedVolume(NamedVolumeMount),
608 Bind(BindMount),
610 Tmpfs(TmpfsMount),
612}
613
614impl From<NamedVolumeMount> for MountIntent {
615 fn from(mount: NamedVolumeMount) -> Self {
616 Self::NamedVolume(mount)
617 }
618}
619
620impl From<BindMount> for MountIntent {
621 fn from(mount: BindMount) -> Self {
622 Self::Bind(mount)
623 }
624}
625
626impl From<TmpfsMount> for MountIntent {
627 fn from(mount: TmpfsMount) -> Self {
628 Self::Tmpfs(mount)
629 }
630}
631
632impl MountIntent {
633 #[must_use]
635 pub fn destination(&self) -> &AbsoluteContainerPath {
636 match self {
637 Self::NamedVolume(mount) => mount.destination(),
638 Self::Bind(mount) => mount.destination(),
639 Self::Tmpfs(mount) => mount.destination(),
640 }
641 }
642
643 #[must_use]
645 pub fn volume_source(&self) -> Option<&DeploymentResourceId> {
646 match self {
647 Self::NamedVolume(mount) => Some(mount.source()),
648 Self::Bind(_) | Self::Tmpfs(_) => None,
649 }
650 }
651}
652
653#[derive(Clone, Copy, Debug, Eq, PartialEq)]
655pub struct UnixId(u32);
656
657impl UnixId {
658 pub fn new(value: u32) -> PodmanLensResult<Self> {
664 if value > i32::MAX as u32 {
665 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
666 }
667 Ok(Self(value))
668 }
669
670 #[must_use]
672 pub const fn get(self) -> u32 {
673 self.0
674 }
675}
676
677#[derive(Clone, Copy, Debug, Eq, PartialEq)]
679pub struct SecretMode(u16);
680
681impl SecretMode {
682 pub fn new(value: u16) -> PodmanLensResult<Self> {
688 if value > 0o777 {
689 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
690 }
691 Ok(Self(value))
692 }
693
694 #[must_use]
696 pub const fn get(self) -> u16 {
697 self.0
698 }
699}
700
701#[derive(Clone, Debug, Eq, PartialEq)]
703#[non_exhaustive]
704pub enum SecretGrant {
705 Mount {
707 source: DeploymentResourceId,
709 target: Option<AbsoluteContainerPath>,
711 uid: Option<UnixId>,
713 gid: Option<UnixId>,
715 mode: Option<SecretMode>,
717 },
718 Environment {
720 source: DeploymentResourceId,
722 target: EnvironmentName,
724 },
725}
726
727impl SecretGrant {
728 pub fn mount(source: DeploymentResourceId) -> PodmanLensResult<Self> {
734 if source.kind() != ResourceKind::Secret {
735 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
736 }
737 Ok(Self::Mount {
738 source,
739 target: None,
740 uid: None,
741 gid: None,
742 mode: None,
743 })
744 }
745
746 pub fn environment(source: DeploymentResourceId, target: EnvironmentName) -> PodmanLensResult<Self> {
752 if source.kind() != ResourceKind::Secret {
753 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
754 }
755 Ok(Self::Environment { source, target })
756 }
757
758 pub fn set_mount_target(&mut self, target: AbsoluteContainerPath) -> PodmanLensResult<()> {
764 match self {
765 Self::Mount { target: slot, .. } if slot.is_none() => {
766 *slot = Some(target);
767 Ok(())
768 }
769 Self::Mount { .. } | Self::Environment { .. } => {
770 Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination))
771 }
772 }
773 }
774
775 pub fn set_mount_uid(&mut self, uid: UnixId) -> PodmanLensResult<()> {
781 set_secret_mount_option(self, uid, |grant| match grant {
782 Self::Mount { uid, .. } => uid,
783 Self::Environment { .. } => unreachable!("environment grants are rejected before access"),
784 })
785 }
786
787 pub fn set_mount_gid(&mut self, gid: UnixId) -> PodmanLensResult<()> {
793 set_secret_mount_option(self, gid, |grant| match grant {
794 Self::Mount { gid, .. } => gid,
795 Self::Environment { .. } => unreachable!("environment grants are rejected before access"),
796 })
797 }
798
799 pub fn set_mount_mode(&mut self, mode: SecretMode) -> PodmanLensResult<()> {
805 set_secret_mount_option(self, mode, |grant| match grant {
806 Self::Mount { mode, .. } => mode,
807 Self::Environment { .. } => unreachable!("environment grants are rejected before access"),
808 })
809 }
810
811 #[must_use]
813 pub fn source(&self) -> &DeploymentResourceId {
814 match self {
815 Self::Mount { source, .. } | Self::Environment { source, .. } => source,
816 }
817 }
818
819 #[must_use]
821 pub fn mount_target(&self) -> Option<&AbsoluteContainerPath> {
822 match self {
823 Self::Mount { target, .. } => target.as_ref(),
824 Self::Environment { .. } => None,
825 }
826 }
827
828 #[must_use]
830 pub fn environment_target(&self) -> Option<&EnvironmentName> {
831 match self {
832 Self::Environment { target, .. } => Some(target),
833 Self::Mount { .. } => None,
834 }
835 }
836
837 #[must_use]
839 pub fn mount_uid(&self) -> Option<UnixId> {
840 match self {
841 Self::Mount { uid, .. } => *uid,
842 Self::Environment { .. } => None,
843 }
844 }
845
846 #[must_use]
848 pub fn mount_gid(&self) -> Option<UnixId> {
849 match self {
850 Self::Mount { gid, .. } => *gid,
851 Self::Environment { .. } => None,
852 }
853 }
854
855 #[must_use]
857 pub fn mount_mode(&self) -> Option<SecretMode> {
858 match self {
859 Self::Mount { mode, .. } => *mode,
860 Self::Environment { .. } => None,
861 }
862 }
863}
864
865fn set_secret_mount_option<T: Eq>(
866 grant: &mut SecretGrant,
867 value: T,
868 member: impl FnOnce(&mut SecretGrant) -> &mut Option<T>,
869) -> PodmanLensResult<()> {
870 if !matches!(grant, SecretGrant::Mount { .. }) {
871 return Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination));
872 }
873 let slot = member(grant);
874 if slot.is_some() {
875 return Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination));
876 }
877 *slot = Some(value);
878 Ok(())
879}
880
881#[derive(Clone, Debug, Default, Eq, PartialEq)]
883pub struct ContainerSettings {
884 command: Option<ArgumentArray>,
885 entrypoint: Option<ArgumentArray>,
886 user: Option<ContainerUser>,
887 workdir: Option<ContainerWorkdir>,
888 hostname: Option<ContainerHostname>,
889 labels: Vec<Label>,
890 environment: Vec<EnvironmentAssignment>,
891 restart_policy: Option<RestartPolicy>,
892}
893
894impl ContainerSettings {
895 pub fn set_command(&mut self, command: ArgumentArray) -> PodmanLensResult<()> {
901 set_once(&mut self.command, command)
902 }
903
904 pub fn set_entrypoint(&mut self, entrypoint: ArgumentArray) -> PodmanLensResult<()> {
910 set_once(&mut self.entrypoint, entrypoint)
911 }
912
913 pub fn set_user(&mut self, user: ContainerUser) -> PodmanLensResult<()> {
919 set_once(&mut self.user, user)
920 }
921
922 pub fn set_workdir(&mut self, workdir: ContainerWorkdir) -> PodmanLensResult<()> {
928 set_once(&mut self.workdir, workdir)
929 }
930
931 pub fn set_hostname(&mut self, hostname: ContainerHostname) -> PodmanLensResult<()> {
937 set_once(&mut self.hostname, hostname)
938 }
939
940 pub fn add_label(&mut self, label: Label) -> PodmanLensResult<()> {
946 if self.labels.len() == MAX_LABELS {
947 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
948 }
949 if self.labels.iter().any(|existing| existing.key == label.key) {
950 return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
951 }
952 self.labels.push(label);
953 Ok(())
954 }
955
956 pub fn add_environment(&mut self, assignment: EnvironmentAssignment) -> PodmanLensResult<()> {
962 if self.environment.len() == MAX_ENVIRONMENT {
963 return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
964 }
965 if self.environment.iter().any(|existing| existing.name == assignment.name) {
966 return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
967 }
968 self.environment.push(assignment);
969 Ok(())
970 }
971
972 pub fn set_restart_policy(&mut self, restart_policy: RestartPolicy) -> PodmanLensResult<()> {
978 set_once(&mut self.restart_policy, restart_policy)
979 }
980
981 #[must_use]
983 pub fn command(&self) -> Option<&ArgumentArray> {
984 self.command.as_ref()
985 }
986
987 #[must_use]
989 pub fn entrypoint(&self) -> Option<&ArgumentArray> {
990 self.entrypoint.as_ref()
991 }
992
993 #[must_use]
995 pub fn user(&self) -> Option<&ContainerUser> {
996 self.user.as_ref()
997 }
998
999 #[must_use]
1001 pub fn workdir(&self) -> Option<&ContainerWorkdir> {
1002 self.workdir.as_ref()
1003 }
1004
1005 #[must_use]
1007 pub fn hostname(&self) -> Option<&ContainerHostname> {
1008 self.hostname.as_ref()
1009 }
1010
1011 #[must_use]
1013 pub fn labels(&self) -> &[Label] {
1014 &self.labels
1015 }
1016
1017 #[must_use]
1019 pub fn environment(&self) -> &[EnvironmentAssignment] {
1020 &self.environment
1021 }
1022
1023 #[must_use]
1025 pub const fn restart_policy(&self) -> Option<RestartPolicy> {
1026 self.restart_policy
1027 }
1028}
1029
1030fn set_once<T: Eq>(slot: &mut Option<T>, value: T) -> PodmanLensResult<()> {
1031 match slot {
1032 None => {
1033 *slot = Some(value);
1034 Ok(())
1035 }
1036 Some(existing) if existing == &value => Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource)),
1037 Some(_) => Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination)),
1038 }
1039}
1040
1041fn valid_value(value: &str) -> bool {
1042 valid_non_control(value, MAX_VALUE_BYTES)
1043}
1044
1045fn valid_non_control(value: &str, maximum_bytes: usize) -> bool {
1046 value.len() <= maximum_bytes && !value.chars().any(char::is_control)
1047}
1048
1049fn valid_user_component(value: &str) -> bool {
1050 !value.is_empty()
1051 && value
1052 .bytes()
1053 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
1054}
1055
1056fn is_absolute_normalized_path(value: &str) -> bool {
1057 value.len() <= MAX_PATH_BYTES
1058 && value.starts_with('/')
1059 && !value.contains('\\')
1060 && !value.chars().any(char::is_control)
1061 && (value == "/"
1062 || value
1063 .split('/')
1064 .skip(1)
1065 .all(|component| !component.is_empty() && component != "." && component != ".."))
1066}