Skip to main content

compose_lens/model/
tmpfs.rs

1//! Raw-preserving service-level temporary-filesystem declarations.
2
3use crate::source::SourceSpan;
4
5use super::Located;
6
7/// One exact service-level `tmpfs` item.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct TmpfsItem {
10    raw: Located<String>,
11    kind: TmpfsItemKind,
12}
13
14impl TmpfsItem {
15    pub(crate) fn parse(raw: Located<String>) -> Self {
16        let kind = classify_tmpfs_item(raw.value());
17        Self { raw, kind }
18    }
19
20    /// Returns the exact string value without normalizing its path or options.
21    #[must_use]
22    pub fn value(&self) -> &str {
23        self.raw.value()
24    }
25
26    /// Returns the exact source span of this scalar item.
27    #[must_use]
28    pub const fn span(&self) -> SourceSpan {
29        self.raw.span()
30    }
31
32    /// Returns the conservative raw-preserving item classification.
33    #[must_use]
34    pub const fn kind(&self) -> TmpfsItemKind {
35        self.kind
36    }
37}
38
39/// The conservative semantic family of one service-level `tmpfs` item.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41#[non_exhaustive]
42pub enum TmpfsItemKind {
43    /// A dollar-bearing item whose effective spelling depends on Compose interpolation.
44    Expression,
45    /// A non-empty path with only non-empty `mode`, `uid`, or `gid` option assignments.
46    Documented,
47    /// An empty or malformed item, or one using provider- or target-specific options.
48    ProviderDependent,
49}
50
51/// The exact authored scalar or sequence form of service-level `tmpfs`.
52#[derive(Debug, Clone, PartialEq, Eq)]
53#[non_exhaustive]
54pub enum TmpfsForm {
55    /// One scalar temporary-filesystem declaration.
56    Scalar(TmpfsItem),
57    /// An explicitly authored sequence, including an explicit empty sequence.
58    List(Vec<TmpfsItem>),
59}
60
61/// An explicitly authored service-level Compose `tmpfs` value.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct Tmpfs {
64    span: SourceSpan,
65    form: TmpfsForm,
66}
67
68impl Tmpfs {
69    pub(crate) const fn new(span: SourceSpan, form: TmpfsForm) -> Self {
70        Self { span, form }
71    }
72
73    /// Returns the exact span of the complete authored field value.
74    #[must_use]
75    pub const fn span(&self) -> SourceSpan {
76        self.span
77    }
78
79    /// Returns the authored scalar or list form without normalizing it.
80    #[must_use]
81    pub const fn form(&self) -> &TmpfsForm {
82        &self.form
83    }
84}
85
86pub(crate) fn classify_tmpfs_item(value: &str) -> TmpfsItemKind {
87    if value.contains('$') {
88        return TmpfsItemKind::Expression;
89    }
90
91    let (path, options) = value
92        .split_once(':')
93        .map_or((value, None), |(path, options)| (path, Some(options)));
94    if path.is_empty() || path.contains(['\r', '\n']) {
95        return TmpfsItemKind::ProviderDependent;
96    }
97
98    let Some(options) = options else {
99        return TmpfsItemKind::Documented;
100    };
101    if options.is_empty() {
102        return TmpfsItemKind::ProviderDependent;
103    }
104
105    for option in options.split(',') {
106        let Some((key, option_value)) = option.split_once('=') else {
107            return TmpfsItemKind::ProviderDependent;
108        };
109        if !matches!(key, "mode" | "uid" | "gid") || option_value.is_empty() || option_value.contains(['\r', '\n']) {
110            return TmpfsItemKind::ProviderDependent;
111        }
112    }
113
114    TmpfsItemKind::Documented
115}
116
117pub(crate) fn valid_generated_tmpfs_item(value: &str) -> bool {
118    if value.contains(['$', '\r', '\n']) {
119        return false;
120    }
121    let (path, options) = value
122        .split_once(':')
123        .map_or((value, None), |(path, options)| (path, Some(options)));
124    if path.is_empty() {
125        return false;
126    }
127    options.is_none_or(|options| {
128        !options.is_empty()
129            && options.split(',').all(|option| {
130                if let Some((key, option_value)) = option.split_once('=') {
131                    !key.is_empty() && !option_value.is_empty()
132                } else {
133                    !option.is_empty()
134                }
135            })
136    })
137}
138
139#[cfg(test)]
140mod tests {
141    use super::{TmpfsItemKind, classify_tmpfs_item, valid_generated_tmpfs_item};
142
143    #[test]
144    fn classifies_exact_documented_expression_and_provider_dependent_spellings() {
145        for value in [
146            "/run",
147            "/path,with,commas",
148            "/run:mode=1777",
149            "relative:uid=user,gid=group",
150            "/run:mode=1=2",
151        ] {
152            assert_eq!(classify_tmpfs_item(value), TmpfsItemKind::Documented);
153        }
154        for value in ["${TMPFS}", "/run:mode=${MODE}"] {
155            assert_eq!(classify_tmpfs_item(value), TmpfsItemKind::Expression);
156        }
157        for value in [
158            "",
159            ":mode=1777",
160            "/run:",
161            "/run:mode",
162            "/run:mode=",
163            "/run:size=64m",
164            "/run:exec,mode=1777",
165            "/run\nnext",
166        ] {
167            assert_eq!(classify_tmpfs_item(value), TmpfsItemKind::ProviderDependent);
168        }
169    }
170
171    #[test]
172    fn generated_items_accept_well_shaped_raw_options_and_duplicates_are_a_collection_concern() {
173        for value in [
174            "/run",
175            "/path,with,commas",
176            "/run:mode=1777,uid=1000,gid=1000",
177            "/run:size=64m",
178            "/run:exec,nosuid,nodev",
179        ] {
180            assert!(
181                valid_generated_tmpfs_item(value),
182                "expected valid generated item {value:?}"
183            );
184        }
185        for value in [
186            "",
187            "${TMPFS}",
188            ":mode=1777",
189            "/run:",
190            "/run:,mode=1777",
191            "/run:mode=",
192            "/run\nnext",
193        ] {
194            assert!(
195                !valid_generated_tmpfs_item(value),
196                "expected invalid generated item {value:?}"
197            );
198        }
199    }
200}