Skip to main content

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}