1pub const MARK: &str = "/* rucc */";
32
33pub const MACRO: &str = "__GLIBC_MINOR__";
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct Releases {
39 minors: Vec<u32>,
40}
41
42impl Releases {
43 pub fn new(minors: Vec<u32>) -> Result<Self, String> {
48 if minors.len() < 2 {
49 return Err("merging wants at least two releases, and one is a copy".to_owned());
50 }
51 for pair in minors.windows(2) {
52 if pair[0] >= pair[1] {
53 return Err(format!(
54 "the releases have to be ascending and distinct, and 2.{} comes after 2.{}",
55 pair[0], pair[1]
56 ));
57 }
58 }
59 Ok(Self { minors })
60 }
61
62 pub fn count(&self) -> usize {
64 self.minors.len()
65 }
66
67 pub fn minors(&self) -> &[u32] {
69 &self.minors
70 }
71
72 pub fn spelled(&self, at: usize) -> String {
74 format!("2.{}", self.minors[at])
75 }
76
77 pub fn condition(&self, present: &[bool]) -> Option<String> {
88 assert_eq!(present.len(), self.minors.len(), "a presence set has one flag per release");
89 if present.iter().all(|&p| p) {
90 return None;
91 }
92 let mut runs: Vec<Vec<String>> = Vec::new();
94 let mut at = 0;
95 while at < present.len() {
96 if !present[at] {
97 at += 1;
98 continue;
99 }
100 let start = at;
101 while at + 1 < present.len() && present[at + 1] {
102 at += 1;
103 }
104 let mut atoms: Vec<String> = Vec::new();
105 if start > 0 {
108 atoms.push(format!("{MACRO} >= {}", self.minors[start]));
109 }
110 if at + 1 < present.len() {
111 atoms.push(format!("{MACRO} < {}", self.minors[at + 1]));
112 }
113 runs.push(atoms);
114 at += 1;
115 }
116 if runs.is_empty() {
117 return Some("0".to_owned());
119 }
120 let one = runs.len() == 1;
123 let terms: Vec<String> = runs
124 .into_iter()
125 .map(|atoms| match atoms.len() {
126 0 => "1".to_owned(),
127 1 => atoms.into_iter().next().unwrap_or_default(),
128 _ if one => atoms.join(" && "),
129 _ => format!("({})", atoms.join(" && ")),
130 })
131 .collect();
132 Some(terms.join(" || "))
133 }
134}
135
136pub fn directive(keyword: &str, condition: Option<&str>) -> String {
138 match condition {
139 Some(text) => format!("#{keyword} {text} {MARK}\n"),
140 None => format!("#{keyword} {MARK}\n"),
141 }
142}
143
144pub fn carries_mark(text: &str) -> bool {
146 text.contains(MARK)
147}
148
149pub fn evaluate(text: &str, minor: u32) -> Result<String, String> {
156 struct Frame {
157 outer: bool,
158 taken: bool,
159 active: bool,
160 }
161 let mut stack: Vec<Frame> = Vec::new();
162 let mut out = String::with_capacity(text.len());
163 for (n, line) in text.split_inclusive('\n').enumerate() {
164 let at = n + 1;
165 let active = stack.last().is_none_or(|f| f.active);
166 let Some(body) = ours(line) else {
167 if active {
168 out.push_str(line);
169 }
170 continue;
171 };
172 if let Some(condition) = body.strip_prefix("#if ") {
173 let holds = holds(condition.trim(), minor).map_err(|why| format!("{at}: {why}"))?;
174 stack.push(Frame { outer: active, taken: holds, active: active && holds });
175 } else if let Some(condition) = body.strip_prefix("#elif ") {
176 let holds = holds(condition.trim(), minor).map_err(|why| format!("{at}: {why}"))?;
177 let frame = stack.last_mut().ok_or(format!("{at}: #elif with no #if"))?;
178 frame.active = frame.outer && !frame.taken && holds;
179 frame.taken = frame.taken || holds;
180 } else if body == "#else" {
181 let frame = stack.last_mut().ok_or(format!("{at}: #else with no #if"))?;
182 frame.active = frame.outer && !frame.taken;
183 frame.taken = true;
184 } else if body == "#endif" {
185 stack.pop().ok_or(format!("{at}: #endif with no #if"))?;
186 } else {
187 return Err(format!("{at}: {body} is marked as ours and is not a directive"));
188 }
189 }
190 if stack.is_empty() {
191 Ok(out)
192 } else {
193 Err(format!("{} of our conditionals are still open at the end", stack.len()))
194 }
195}
196
197fn ours(line: &str) -> Option<&str> {
199 let body = line.trim_end();
200 let body = body.strip_suffix(MARK)?.trim_end();
201 body.starts_with('#').then_some(body)
202}
203
204fn holds(condition: &str, minor: u32) -> Result<bool, String> {
206 let mut any = false;
207 for term in condition.split("||") {
208 let term = term.trim();
209 let term = match term.strip_prefix('(') {
210 Some(rest) => {
211 rest.strip_suffix(')').ok_or(format!("unbalanced parentheses: {term}"))?
212 }
213 None => term,
214 };
215 let mut all = true;
216 for atom in term.split("&&") {
217 all &= atom_holds(atom.trim(), minor)?;
218 }
219 any |= all;
220 }
221 Ok(any)
222}
223
224fn atom_holds(atom: &str, minor: u32) -> Result<bool, String> {
226 match atom {
227 "1" => return Ok(true),
228 "0" => return Ok(false),
229 _ => {}
230 }
231 let rest = atom.strip_prefix(MACRO).ok_or(format!("not a condition of ours: {atom}"))?;
232 let rest = rest.trim_start();
233 if let Some(value) = rest.strip_prefix(">=") {
234 Ok(minor >= number(value)?)
235 } else if let Some(value) = rest.strip_prefix('<') {
236 Ok(minor < number(value)?)
237 } else {
238 Err(format!("not a comparison of ours: {atom}"))
239 }
240}
241
242fn number(text: &str) -> Result<u32, String> {
244 text.trim().parse().map_err(|_| format!("not a version: {text}"))
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 const EIGHT: [u32; 8] = [28, 31, 34, 35, 36, 39, 41, 44];
252
253 fn releases() -> Releases {
254 Releases::new(EIGHT.to_vec()).expect("ascending and distinct")
255 }
256
257 fn present(of: &[u32]) -> Vec<bool> {
258 EIGHT.iter().map(|m| of.contains(m)).collect()
259 }
260
261 fn holding(condition: &Option<String>) -> Vec<u32> {
264 EIGHT
265 .iter()
266 .copied()
267 .filter(|&m| match condition {
268 None => true,
269 Some(text) => holds(text, m).expect("our own grammar"),
270 })
271 .collect()
272 }
273
274 #[test]
275 fn every_subset_of_eight_releases_gets_a_condition_that_means_it() {
276 let all = releases();
277 for bits in 0u32..256 {
278 let chosen: Vec<u32> = EIGHT
279 .iter()
280 .enumerate()
281 .filter(|(n, _)| bits & (1 << n) != 0)
282 .map(|(_, &m)| m)
283 .collect();
284 let condition = all.condition(&present(&chosen));
285 assert_eq!(holding(&condition), chosen, "{condition:?}");
286 }
287 }
288
289 #[test]
290 fn the_whole_set_wants_no_conditional() {
291 assert_eq!(releases().condition(&[true; 8]), None);
292 }
293
294 #[test]
295 fn the_shapes_a_reviewer_reads() {
296 let all = releases();
297 assert_eq!(all.condition(&present(&[44])), Some("__GLIBC_MINOR__ >= 44".to_owned()));
298 assert_eq!(
299 all.condition(&present(&[39, 41, 44])),
300 Some("__GLIBC_MINOR__ >= 39".to_owned())
301 );
302 assert_eq!(all.condition(&present(&[28])), Some("__GLIBC_MINOR__ < 31".to_owned()));
303 assert_eq!(all.condition(&present(&[28, 31])), Some("__GLIBC_MINOR__ < 34".to_owned()));
304 assert_eq!(
305 all.condition(&present(&[34, 35])),
306 Some("__GLIBC_MINOR__ >= 34 && __GLIBC_MINOR__ < 36".to_owned())
307 );
308 assert_eq!(
309 all.condition(&present(&[28, 41, 44])),
310 Some("__GLIBC_MINOR__ < 31 || __GLIBC_MINOR__ >= 41".to_owned())
311 );
312 assert_eq!(
313 all.condition(&present(&[31, 34, 44])),
314 Some(
315 "(__GLIBC_MINOR__ >= 31 && __GLIBC_MINOR__ < 35) || __GLIBC_MINOR__ >= 44"
316 .to_owned()
317 )
318 );
319 }
320
321 #[test]
324 fn a_release_between_two_surveyed_ones_belongs_to_the_older() {
325 let all = releases();
326 let condition = all.condition(&present(&[28, 31])).expect("not everything");
327 for minor in [0, 17, 28, 30, 31, 33] {
328 assert!(holds(&condition, minor).expect("ours"), "2.{minor}");
329 }
330 for minor in [34, 35, 44, 99] {
331 assert!(!holds(&condition, minor).expect("ours"), "2.{minor}");
332 }
333 }
334
335 #[test]
336 fn a_run_of_two_releases_and_nothing_else_is_rejected_as_a_set_of_one() {
337 assert!(Releases::new(vec![28]).is_err());
338 assert!(Releases::new(vec![31, 28]).is_err());
339 assert!(Releases::new(vec![28, 28]).is_err());
340 assert!(Releases::new(vec![28, 31]).is_ok());
341 }
342
343 #[test]
344 fn a_conditional_is_read_back_the_way_it_was_written() {
345 let text = format!(
346 "common\n{}new\n{}old\n{}tail\n",
347 directive("if", Some("__GLIBC_MINOR__ >= 34")),
348 directive("else", None),
349 directive("endif", None),
350 );
351 assert_eq!(evaluate(&text, 34).expect("ours"), "common\nnew\ntail\n");
352 assert_eq!(evaluate(&text, 31).expect("ours"), "common\nold\ntail\n");
353 }
354
355 #[test]
356 fn an_elif_chain_takes_the_first_branch_that_holds_and_no_other() {
357 let text = format!(
358 "{}a\n{}b\n{}c\n{}",
359 directive("if", Some("__GLIBC_MINOR__ >= 41")),
360 directive("elif", Some("__GLIBC_MINOR__ >= 34")),
361 directive("else", None),
362 directive("endif", None),
363 );
364 assert_eq!(evaluate(&text, 44).expect("ours"), "a\n");
365 assert_eq!(evaluate(&text, 36).expect("ours"), "b\n");
366 assert_eq!(evaluate(&text, 28).expect("ours"), "c\n");
367 }
368
369 #[test]
371 fn the_files_own_conditionals_are_left_alone() {
372 let text = format!(
373 "#ifdef __USE_GNU\n{}int f (void);\n{}#endif\n",
374 directive("if", Some("__GLIBC_MINOR__ >= 34")),
375 directive("endif", None),
376 );
377 assert_eq!(evaluate(&text, 44).expect("ours"), "#ifdef __USE_GNU\nint f (void);\n#endif\n");
378 assert_eq!(evaluate(&text, 28).expect("ours"), "#ifdef __USE_GNU\n#endif\n");
379 }
380
381 #[test]
382 fn a_branch_inside_a_branch_that_is_not_taken_stays_shut() {
383 let text = format!(
384 "{}outer\n{}inner\n{}{}",
385 directive("if", Some("__GLIBC_MINOR__ >= 41")),
386 directive("if", Some("__GLIBC_MINOR__ >= 44")),
387 directive("endif", None),
388 directive("endif", None),
389 );
390 assert_eq!(evaluate(&text, 44).expect("ours"), "outer\ninner\n");
391 assert_eq!(evaluate(&text, 41).expect("ours"), "outer\n");
392 assert_eq!(evaluate(&text, 28).expect("ours"), "");
393 }
394
395 #[test]
396 fn an_unfinished_conditional_of_ours_is_an_error_and_not_a_guess() {
397 let text = directive("if", Some("__GLIBC_MINOR__ >= 34"));
398 assert!(evaluate(&text, 34).is_err());
399 assert!(evaluate(&directive("endif", None), 34).is_err());
400 assert!(evaluate(&directive("else", None), 34).is_err());
401 }
402
403 #[test]
404 fn a_condition_we_did_not_write_is_an_error() {
405 let text = format!("#if defined __USE_GNU {MARK}\n{}", directive("endif", None));
406 let why = evaluate(&text, 34).expect_err("not our grammar");
407 assert!(why.contains("not a condition of ours"), "{why}");
408 }
409
410 #[test]
411 fn the_marker_is_what_a_tree_is_refused_for_carrying() {
412 assert!(carries_mark(&directive("endif", None)));
413 assert!(!carries_mark("#endif /* features.h */\n"));
414 }
415}