Skip to main content

compose_lens/model/
identity.rs

1//! Raw-preserving container identity and user-namespace values.
2
3use super::Located;
4
5/// A service `user` value with optional user/group decomposition.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct UserSpec {
8    raw: Located<String>,
9    user: IdentityComponent,
10    group: Option<IdentityComponent>,
11}
12
13impl UserSpec {
14    pub(crate) fn parse(raw: Located<String>) -> Self {
15        let (user, group) = split_user_group(raw.value())
16            .map_or_else(|| (raw.value().as_str(), None), |(user, group)| (user, Some(group)));
17        Self {
18            user: IdentityComponent::parse(user),
19            group: group.map(IdentityComponent::parse),
20            raw,
21        }
22    }
23
24    /// Returns the complete authored scalar.
25    #[must_use]
26    pub const fn raw(&self) -> &Located<String> {
27        &self.raw
28    }
29
30    /// Returns the user component without resolving names or IDs.
31    #[must_use]
32    pub const fn user(&self) -> &IdentityComponent {
33        &self.user
34    }
35
36    /// Returns the optional group component.
37    #[must_use]
38    pub const fn group(&self) -> Option<&IdentityComponent> {
39        self.group.as_ref()
40    }
41}
42
43/// One lexical component of a container user/group value.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum IdentityComponent {
46    /// An unsigned numeric UID or GID, with spelling retained.
47    Numeric(String),
48    /// A user or group name.
49    Name(String),
50    /// A deferred Compose interpolation expression.
51    Expression(String),
52    /// An explicitly empty component.
53    Empty,
54}
55
56impl IdentityComponent {
57    fn parse(value: &str) -> Self {
58        if value.is_empty() {
59            Self::Empty
60        } else if value.contains("${") || value.contains("$$") {
61            Self::Expression(value.to_owned())
62        } else if value.bytes().all(|byte| byte.is_ascii_digit()) {
63            Self::Numeric(value.to_owned())
64        } else {
65            Self::Name(value.to_owned())
66        }
67    }
68
69    /// Returns the retained component spelling.
70    #[must_use]
71    pub fn raw(&self) -> &str {
72        match self {
73            Self::Numeric(value) | Self::Name(value) | Self::Expression(value) => value,
74            Self::Empty => "",
75        }
76    }
77}
78
79/// A service `userns_mode` value.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct UserNamespaceMode {
82    raw: Located<String>,
83    kind: UserNamespaceModeKind,
84}
85
86impl UserNamespaceMode {
87    pub(crate) fn parse(raw: Located<String>) -> Self {
88        let kind = match raw.value().as_str() {
89            "host" | "" => UserNamespaceModeKind::Host,
90            value if value == "keep-id" || value.starts_with("keep-id:") => UserNamespaceModeKind::PodmanKeepId,
91            value if value == "auto" || value.starts_with("auto:") => UserNamespaceModeKind::PodmanAuto,
92            "nomap" => UserNamespaceModeKind::PodmanNoMap,
93            value if value.starts_with("container:") => UserNamespaceModeKind::Container,
94            _ => UserNamespaceModeKind::Other,
95        };
96        Self { raw, kind }
97    }
98
99    /// Returns the complete authored scalar.
100    #[must_use]
101    pub const fn raw(&self) -> &Located<String> {
102        &self.raw
103    }
104
105    /// Returns the non-destructive mode classification.
106    #[must_use]
107    pub const fn kind(&self) -> UserNamespaceModeKind {
108        self.kind
109    }
110
111    /// Reports whether the value selects a Podman-specific namespace mode.
112    #[must_use]
113    pub const fn is_podman_specific(&self) -> bool {
114        matches!(
115            self.kind,
116            UserNamespaceModeKind::PodmanKeepId
117                | UserNamespaceModeKind::PodmanAuto
118                | UserNamespaceModeKind::PodmanNoMap
119        )
120    }
121}
122
123/// The recognized family of a `userns_mode` value.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125pub enum UserNamespaceModeKind {
126    /// Host user namespace.
127    Host,
128    /// Podman's `keep-id` mode and options.
129    PodmanKeepId,
130    /// Podman's `auto` mode and options.
131    PodmanAuto,
132    /// Podman's `nomap` mode.
133    PodmanNoMap,
134    /// A container namespace reference.
135    Container,
136    /// A deferred or provider-specific value.
137    Other,
138}
139
140fn split_user_group(value: &str) -> Option<(&str, &str)> {
141    let bytes = value.as_bytes();
142    let mut interpolation_depth = 0_usize;
143    let mut index = 0_usize;
144    while index < bytes.len() {
145        if bytes[index] == b'$' && bytes.get(index + 1) == Some(&b'{') {
146            interpolation_depth += 1;
147            index += 2;
148            continue;
149        }
150        if bytes[index] == b'}' && interpolation_depth > 0 {
151            interpolation_depth -= 1;
152        } else if bytes[index] == b':' && interpolation_depth == 0 {
153            return Some((&value[..index], &value[index + 1..]));
154        }
155        index += 1;
156    }
157    None
158}
159
160#[cfg(test)]
161mod tests {
162    use super::{IdentityComponent, UserSpec};
163    use crate::model::Located;
164    use crate::source::{SourceId, SourceSpan};
165
166    #[test]
167    fn does_not_split_interpolation_default_operators() -> Result<(), &'static str> {
168        let value = "${UID:-1000}:${GID:-1000}";
169        let span = SourceSpan::new(SourceId::new(1), 0, value.len()).ok_or("valid test span expected")?;
170        let parsed = UserSpec::parse(Located::new(value.to_owned(), span));
171        assert_eq!(parsed.user(), &IdentityComponent::Expression("${UID:-1000}".to_owned()));
172        assert_eq!(
173            parsed.group(),
174            Some(&IdentityComponent::Expression("${GID:-1000}".to_owned()))
175        );
176        Ok(())
177    }
178}