1use crate::source::SourceSpan;
4
5use super::Located;
6
7#[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 #[must_use]
22 pub fn value(&self) -> &str {
23 self.raw.value()
24 }
25
26 #[must_use]
28 pub const fn span(&self) -> SourceSpan {
29 self.raw.span()
30 }
31
32 #[must_use]
34 pub const fn kind(&self) -> TmpfsItemKind {
35 self.kind
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41#[non_exhaustive]
42pub enum TmpfsItemKind {
43 Expression,
45 Documented,
47 ProviderDependent,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53#[non_exhaustive]
54pub enum TmpfsForm {
55 Scalar(TmpfsItem),
57 List(Vec<TmpfsItem>),
59}
60
61#[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 #[must_use]
75 pub const fn span(&self) -> SourceSpan {
76 self.span
77 }
78
79 #[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}