1use super::Located;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct ShmSize {
8 raw: Located<String>,
9 scalar_kind: ShmSizeScalarKind,
10 kind: ShmSizeKind,
11}
12
13impl ShmSize {
14 pub(crate) fn parse(raw: Located<String>, scalar_kind: ShmSizeScalarKind) -> Self {
15 let value = raw.value();
16 let kind = if matches!(scalar_kind, ShmSizeScalarKind::String) && value.contains('$') {
17 ShmSizeKind::Expression
18 } else if let Some((amount_raw, unit)) = split_documented_unit(value) {
19 if lexical_zero(amount_raw) {
20 ShmSizeKind::Zero {
21 amount_raw: amount_raw.to_owned(),
22 unit: Some(unit),
23 }
24 } else {
25 ShmSizeKind::Documented {
26 amount_raw: amount_raw.to_owned(),
27 unit,
28 }
29 }
30 } else if lexical_zero(value) {
31 ShmSizeKind::Zero {
32 amount_raw: value.to_owned(),
33 unit: None,
34 }
35 } else {
36 match scalar_kind {
37 ShmSizeScalarKind::Number => ShmSizeKind::ProviderDependentNumber,
38 ShmSizeScalarKind::String => ShmSizeKind::ProviderDependentString,
39 }
40 };
41 Self { raw, scalar_kind, kind }
42 }
43
44 #[must_use]
46 pub const fn raw(&self) -> &Located<String> {
47 &self.raw
48 }
49
50 #[must_use]
52 pub const fn scalar_kind(&self) -> ShmSizeScalarKind {
53 self.scalar_kind
54 }
55
56 #[must_use]
58 pub const fn kind(&self) -> &ShmSizeKind {
59 &self.kind
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65#[non_exhaustive]
66pub enum ShmSizeScalarKind {
67 Number,
69 String,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75#[non_exhaustive]
76pub enum ShmSizeKind {
77 Documented {
79 amount_raw: String,
81 unit: ShmSizeUnit,
83 },
84 Zero {
86 amount_raw: String,
88 unit: Option<ShmSizeUnit>,
90 },
91 Expression,
93 ProviderDependentNumber,
95 ProviderDependentString,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101#[non_exhaustive]
102pub enum ShmSizeUnit {
103 B,
105 K,
107 Kb,
109 M,
111 Mb,
113 G,
115 Gb,
117}
118
119impl ShmSizeUnit {
120 #[must_use]
122 pub const fn as_str(self) -> &'static str {
123 match self {
124 Self::B => "b",
125 Self::K => "k",
126 Self::Kb => "kb",
127 Self::M => "m",
128 Self::Mb => "mb",
129 Self::G => "g",
130 Self::Gb => "gb",
131 }
132 }
133}
134
135pub(crate) fn valid_generated_shm_amount(value: &str) -> bool {
136 value
137 .as_bytes()
138 .split_first()
139 .is_some_and(|(first, rest)| (b'1'..=b'9').contains(first) && rest.iter().all(u8::is_ascii_digit))
140}
141
142fn split_documented_unit(value: &str) -> Option<(&str, ShmSizeUnit)> {
143 for (suffix, unit) in [
144 ("kb", ShmSizeUnit::Kb),
145 ("mb", ShmSizeUnit::Mb),
146 ("gb", ShmSizeUnit::Gb),
147 ("b", ShmSizeUnit::B),
148 ("k", ShmSizeUnit::K),
149 ("m", ShmSizeUnit::M),
150 ("g", ShmSizeUnit::G),
151 ] {
152 if let Some(amount) = value.strip_suffix(suffix) {
153 if !amount.is_empty() {
154 return Some((amount, unit));
155 }
156 }
157 }
158 None
159}
160
161fn lexical_zero(value: &str) -> bool {
162 !value.is_empty() && value.bytes().all(|byte| byte == b'0')
163}
164
165#[cfg(test)]
166mod tests {
167 use super::{ShmSize, ShmSizeKind, ShmSizeScalarKind, ShmSizeUnit, valid_generated_shm_amount};
168 use crate::model::Located;
169 use crate::source::{SourceId, SourceSpan};
170
171 fn classify(value: &str, scalar_kind: ShmSizeScalarKind) -> Result<ShmSize, &'static str> {
172 let span = SourceSpan::new(SourceId::new(1), 0, value.len()).ok_or("valid test span expected")?;
173 Ok(ShmSize::parse(Located::new(value.to_owned(), span), scalar_kind))
174 }
175
176 #[test]
177 fn retains_documented_suffixes_without_inventing_an_amount_grammar() -> Result<(), &'static str> {
178 for (value, amount_raw, unit) in [
179 ("1b", "1", ShmSizeUnit::B),
180 ("01k", "01", ShmSizeUnit::K),
181 ("+1kb", "+1", ShmSizeUnit::Kb),
182 ("1.5m", "1.5", ShmSizeUnit::M),
183 ("1e3mb", "1e3", ShmSizeUnit::Mb),
184 ("-2g", "-2", ShmSizeUnit::G),
185 ("hugegb", "huge", ShmSizeUnit::Gb),
186 ] {
187 let size = classify(value, ShmSizeScalarKind::String)?;
188 assert_eq!(size.raw().value(), value);
189 assert!(matches!(
190 size.kind(),
191 ShmSizeKind::Documented { amount_raw: actual, unit: actual_unit }
192 if actual == amount_raw && *actual_unit == unit
193 ));
194 }
195 Ok(())
196 }
197
198 #[test]
199 fn keeps_zero_expressions_and_schema_scalar_categories_distinct() -> Result<(), &'static str> {
200 assert!(matches!(
201 classify("000mb", ShmSizeScalarKind::String)?.kind(),
202 ShmSizeKind::Zero { amount_raw, unit: Some(ShmSizeUnit::Mb) } if amount_raw == "000"
203 ));
204 assert!(matches!(
205 classify("0", ShmSizeScalarKind::Number)?.kind(),
206 ShmSizeKind::Zero { amount_raw, unit: None } if amount_raw == "0"
207 ));
208 assert_eq!(
209 classify("${SHM_SIZE:-64m}", ShmSizeScalarKind::String)?.kind(),
210 &ShmSizeKind::Expression
211 );
212 assert_eq!(
213 classify("64", ShmSizeScalarKind::Number)?.kind(),
214 &ShmSizeKind::ProviderDependentNumber
215 );
216 assert_eq!(
217 classify("64", ShmSizeScalarKind::String)?.kind(),
218 &ShmSizeKind::ProviderDependentString
219 );
220 Ok(())
221 }
222
223 #[test]
224 fn validates_only_canonical_positive_generated_amounts() {
225 for value in ["1", "64", "18446744073709551616000000000000000000000000000000"] {
226 assert!(valid_generated_shm_amount(value));
227 }
228 for value in ["", "0", "00", "01", "-1", "+1", "1.0", "1e3", " 1", "1 ", "${SIZE}"] {
229 assert!(!valid_generated_shm_amount(value));
230 }
231 }
232}