rucc_headers/norm.rs
1//! What counts as a change to a header, and what a header is cut into before it is merged.
2//!
3//! Design: `spec/cross-compile/08-sysroots.md` section 8.3, the paragraph that says a change is a
4//! change to the code and not to the bytes. 420 of glibc's 498 installed headers differ in bytes
5//! somewhere between 2.28 and 2.44 and only 210 of them differ in code, because glibc moves the
6//! copyright line in every file every January and changed the licence URL in every file from
7//! `http` to `https` in 2019. A merge that believed the bytes would write a conditional into twice
8//! as many files as need one, and every one of those conditionals would be about a year.
9//!
10//! So two texts are compared by their code: comments removed, blank lines dropped, continued lines
11//! joined, runs of spaces and tabs collapsed. The text that gets written out is still real header
12//! text, never a normalized form of one, and where releases agree on the code it is the newest
13//! release's text that is written.
14//!
15//! # Why a file is cut into logical lines and not into lines
16//!
17//! Because the merge writes `#if` between two pieces and there are places that cannot have an
18//! `#if` put in the middle of them. One is a macro definition continued with a backslash, where a
19//! directive between the continuation lines is not a directive at all. Another is a directive
20//! continued the same way. The third is a block comment that opens after some code and closes on a
21//! later line, which glibc writes in every table of constants:
22//!
23//! ```text
24//! #define IN_EXCL_UNLINK 0x04000000 /* Exclude events on unlinked
25//! objects. */
26//! #define IN_MASK_ADD 0x20000000 /* Add to the mask. */
27//! ```
28//!
29//! A cut between those two lines puts the end of a comment at the top of a piece, and the release
30//! that does not take that piece is left with a comment that never closes and a file whose next
31//! several declarations are inside it. So a logical line runs until the line ends with no comment
32//! open and no backslash on it, and a conditional lands before it or after it and never inside it.
33//!
34//! Comment-only and blank lines are not pieces of their own either. They attach to the code line
35//! below them, which is where the comment about it lives, so moving a declaration between releases
36//! moves its comment with it instead of leaving it stranded above a conditional.
37
38/// One piece of a header: a logical line of code, with whatever comments and blank lines came
39/// immediately above it.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Item {
42 /// The text exactly as the file had it, newlines included, which is what gets written out.
43 pub text: String,
44 /// The code of the logical line, normalized, which is what two releases are compared by.
45 pub key: String,
46 /// Where in `text` the logical line starts, which is after the comments that came with it.
47 ///
48 /// Here so that a patch to the code can leave the comment above it where it was, which is what
49 /// the replacement of glibc's own definition of `__GLIBC_MINOR__` does.
50 pub code_at: usize,
51}
52
53/// A header cut into pieces, with the comments after the last piece kept separately.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Pieces {
56 /// The pieces, in order.
57 pub items: Vec<Item>,
58 /// The trailing comments and blank lines, which belong to no code line.
59 pub tail: String,
60}
61
62impl Pieces {
63 /// The comparison keys, for the alignment.
64 pub fn keys(&self) -> Vec<&str> {
65 self.items.iter().map(|i| i.key.as_str()).collect()
66 }
67}
68
69/// Cuts a header into pieces.
70///
71/// Every byte of the input is in exactly one piece's `text` or in `tail`, in order, so
72/// concatenating them all reproduces the file. That is the property the merge relies on: a piece
73/// is a place the text can be cut, not a summary of what is there.
74pub fn pieces(text: &str) -> Pieces {
75 let mut scanner = Scanner::default();
76 let mut items: Vec<Item> = Vec::new();
77 let mut pending = String::new();
78 let mut logical = String::new();
79 let mut key = String::new();
80
81 for line in text.split_inclusive('\n') {
82 let code = scanner.line(line);
83 let continues = continued(line);
84 if logical.is_empty() && code.is_empty() && !continues {
85 // A comment or a blank line, which belongs to the next piece of code.
86 pending.push_str(line);
87 continue;
88 }
89 logical.push_str(line);
90 if !code.is_empty() {
91 if !key.is_empty() {
92 key.push(' ');
93 }
94 key.push_str(&code);
95 }
96 if continues || scanner.in_comment {
97 continue;
98 }
99 let mut piece = std::mem::take(&mut pending);
100 let code_at = piece.len();
101 piece.push_str(&logical);
102 logical.clear();
103 items.push(Item { text: piece, key: std::mem::take(&mut key), code_at });
104 }
105
106 // A file whose last line is continued, or whose last line has no newline on it, still has to
107 // come back out whole.
108 if !logical.is_empty() {
109 let mut piece = std::mem::take(&mut pending);
110 let code_at = piece.len();
111 piece.push_str(&logical);
112 items.push(Item { text: piece, key: std::mem::take(&mut key), code_at });
113 }
114 Pieces { items, tail: pending }
115}
116
117/// The code of one text, which is what "the same header" means here.
118///
119/// One logical line per line, which is the same cut [`pieces`] makes and for the same reason: the
120/// preprocessor joins a continued line before anything looks at it, so a macro whose body had its
121/// line break moved is the same macro. There is one definition of the code and both the alignment
122/// and the check the merge does afterwards use it, because two definitions that disagree anywhere
123/// would have the merge writing a file it then says is wrong.
124pub fn code(text: &str) -> String {
125 let mut out = String::new();
126 for item in &pieces(text).items {
127 if item.key.is_empty() {
128 continue;
129 }
130 if !out.is_empty() {
131 out.push('\n');
132 }
133 out.push_str(&item.key);
134 }
135 out
136}
137
138/// Whether this physical line is continued on the next one.
139fn continued(line: &str) -> bool {
140 line.trim_end_matches(['\n', '\r']).ends_with('\\')
141}
142
143/// The comment state carried from one line to the next.
144#[derive(Default)]
145struct Scanner {
146 in_comment: bool,
147}
148
149impl Scanner {
150 /// The code on one physical line, with the comments taken out and the spacing collapsed.
151 fn line(&mut self, text: &str) -> String {
152 let bytes = text.as_bytes();
153 let mut out = String::with_capacity(text.len());
154 let mut i = 0;
155 while i < bytes.len() {
156 if self.in_comment {
157 if bytes[i] == b'*' && bytes.get(i + 1) == Some(&b'/') {
158 self.in_comment = false;
159 i += 2;
160 } else {
161 i += 1;
162 }
163 continue;
164 }
165 match bytes[i] {
166 b'/' if bytes.get(i + 1) == Some(&b'*') => {
167 self.in_comment = true;
168 i += 2;
169 // A comment between two tokens is a space between them, not nothing.
170 push_space(&mut out);
171 }
172 b'/' if bytes.get(i + 1) == Some(&b'/') => break,
173 b'"' | b'\'' => {
174 let quote = bytes[i];
175 match literal(bytes, i) {
176 // A string or a character constant is code, and a `/*` inside one is not
177 // the start of a comment.
178 Some(end) => {
179 out.push_str(&text[i..end]);
180 i = end;
181 }
182 // An apostrophe in prose and an unterminated string are the same thing
183 // here: a byte, taken as it stands. glibc's headers have both, in
184 // `#error` text and in what a comment scanner would otherwise swallow.
185 None => {
186 out.push(quote as char);
187 i += 1;
188 }
189 }
190 }
191 b' ' | b'\t' | b'\r' | b'\n' => {
192 push_space(&mut out);
193 i += 1;
194 }
195 b'\\' if i + 1 == bytes.len() || bytes[i + 1] == b'\n' || bytes[i + 1] == b'\r' => {
196 // The backslash of a continued line is spacing rather than code, so that a
197 // macro whose body moved to one line reads the same as one that did not.
198 push_space(&mut out);
199 i += 1;
200 }
201 byte => {
202 out.push(byte as char);
203 i += 1;
204 }
205 }
206 }
207 out.trim().to_owned()
208 }
209}
210
211/// One space, and never two in a row or one at the start.
212fn push_space(out: &mut String) {
213 if !out.is_empty() && !out.ends_with(' ') {
214 out.push(' ');
215 }
216}
217
218/// The end of the literal that starts at `at`, if it ends on this line.
219fn literal(bytes: &[u8], at: usize) -> Option<usize> {
220 let quote = bytes[at];
221 let mut i = at + 1;
222 while i < bytes.len() {
223 match bytes[i] {
224 b'\\' => i += 2,
225 b'\n' => return None,
226 byte if byte == quote => return Some(i + 1),
227 _ => i += 1,
228 }
229 }
230 None
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 /// Every byte of the input is in one piece or in the tail, which is what lets the merge cut
238 /// a file up and write the pieces back out.
239 fn rejoined(text: &str) -> String {
240 let cut = pieces(text);
241 let mut out: String = cut.items.iter().map(|i| i.text.as_str()).collect();
242 out.push_str(&cut.tail);
243 out
244 }
245
246 #[test]
247 fn a_file_comes_back_out_of_its_pieces() {
248 for text in [
249 "#define A 1\n",
250 "/* one */\n#define A 1\n/* two */\n",
251 "#define A \\\n 1\n",
252 "no newline at the end",
253 "\n\n",
254 "",
255 ] {
256 assert_eq!(rejoined(text), text, "{text:?}");
257 }
258 }
259
260 #[test]
261 fn a_comment_above_a_declaration_belongs_to_it() {
262 let cut = pieces("/* what it does\n and why */\nint f (void);\n#define A 1\n");
263 assert_eq!(cut.items.len(), 2);
264 assert!(cut.items[0].text.starts_with("/* what it does"));
265 assert_eq!(&cut.items[0].text[cut.items[0].code_at..], "int f (void);\n");
266 assert_eq!(cut.items[0].key, "int f (void);");
267 assert_eq!(cut.items[1].key, "#define A 1");
268 assert_eq!(cut.tail, "");
269 }
270
271 #[test]
272 fn the_comments_at_the_end_belong_to_nothing() {
273 let cut = pieces("int f (void);\n/* the end */\n");
274 assert_eq!(cut.items.len(), 1);
275 assert_eq!(cut.tail, "/* the end */\n");
276 }
277
278 /// The hazard this cutting exists for: a conditional cannot go between these two lines.
279 #[test]
280 fn a_continued_macro_is_one_piece() {
281 let cut = pieces("#define TWO(a, b) \\\n do { a; b; } while (0)\n#define A 1\n");
282 assert_eq!(cut.items.len(), 2);
283 assert_eq!(cut.items[0].key, "#define TWO(a, b) do { a; b; } while (0)");
284 assert_eq!(cut.items[1].key, "#define A 1");
285 }
286
287 #[test]
288 fn the_year_in_the_copyright_line_is_not_code() {
289 let old = "/* Copyright (C) 1991-2018 Free Software Foundation, Inc.\n http://x */\n\
290 #define A 1\n";
291 let new = "/* Copyright (C) 1991-2024 Free Software Foundation, Inc.\n https://x */\n\
292 #define A 1\n";
293 assert_ne!(old, new);
294 assert_eq!(code(old), code(new));
295 assert_eq!(code(old), "#define A 1");
296 }
297
298 #[test]
299 fn a_comment_between_two_tokens_is_a_space() {
300 assert_eq!(code("int/* and */f (void);\n"), "int f (void);");
301 assert_eq!(code("int \tf (void);\n"), "int f (void);");
302 }
303
304 #[test]
305 fn a_comment_start_inside_a_string_is_not_one() {
306 assert_eq!(code("#define S \"/*\"\n#define A 1\n"), "#define S \"/*\"\n#define A 1");
307 }
308
309 /// glibc's `features.h` has apostrophes in its prose, and a scanner that took one for a
310 /// character constant would eat the rest of the line and then agree with a release that
311 /// changed it.
312 #[test]
313 fn an_apostrophe_in_a_comment_does_not_eat_the_file() {
314 let text = "/* The macros `__GLIBC__' and `__GLIBC_MINOR__' are defined. */\n#define A 1\n";
315 assert_eq!(code(text), "#define A 1");
316 }
317
318 #[test]
319 fn a_line_comment_ends_the_code_on_its_line() {
320 assert_eq!(
321 code("#define A 1 // and nothing after\n#define B 2\n"),
322 "#define A 1\n#define B 2"
323 );
324 }
325
326 #[test]
327 fn a_comment_that_spans_lines_is_gone_from_all_of_them() {
328 assert_eq!(
329 code("#define A 1\n/* one\n two\n three */\n#define B 2\n"),
330 "#define A 1\n#define B 2"
331 );
332 }
333
334 /// The hazard the module documentation opens with. A cut between these two definitions would
335 /// leave the release that does not take the second one with a comment that never closes.
336 #[test]
337 fn a_comment_that_opens_after_code_keeps_its_piece_open() {
338 let text = "#define A 1\t/* one\n\t\t\t two. */\n#define B 2\n";
339 let cut = pieces(text);
340 assert_eq!(cut.items.len(), 2);
341 assert_eq!(cut.items[0].text, "#define A 1\t/* one\n\t\t\t two. */\n");
342 assert_eq!(cut.items[0].key, "#define A 1");
343 assert_eq!(cut.items[1].text, "#define B 2\n");
344 assert_eq!(rejoined(text), text);
345 }
346
347 /// Moving where a continued line breaks is not a change, because the preprocessor joins the
348 /// line before anything sees it. glibc does this in `a.out.h`, `dirent.h` and `ip_icmp.h`
349 /// between 2.28 and 2.44, always to put an operator at the start of a line instead of the end.
350 #[test]
351 fn moving_where_a_continued_line_breaks_is_not_a_change() {
352 let old = "#define F(x) \\\n (a (x) ? b (x) : \\\n c (x))\n";
353 let new = "#define F(x) \\\n (a (x) ? b (x) \\\n : c (x))\n";
354 assert_eq!(code(old), code(new));
355 assert_eq!(code(old), "#define F(x) (a (x) ? b (x) : c (x))");
356 }
357}