xtask/tasks/fmt/house_rules/
copyright.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use anyhow::anyhow;
5use fs_err::File;
6use std::io::BufRead;
7use std::io::BufReader;
8use std::io::Read;
9use std::io::Write;
10use std::path::Path;
11
12fn commit(source: File, target: &Path) -> std::io::Result<()> {
13    source.set_permissions(target.metadata()?.permissions())?;
14    let (file, path) = source.into_parts();
15    drop(file); // Windows requires the source be closed in some cases.
16    fs_err::rename(path, target)
17}
18
19pub fn check_copyright(path: &Path, fix: bool) -> anyhow::Result<()> {
20    const HEADER_MIT_FIRST: &str = "Copyright (c) Microsoft Corporation.";
21    const HEADER_MIT_SECOND: &str = "Licensed under the MIT License.";
22
23    let ext = path
24        .extension()
25        .and_then(|e| e.to_str())
26        .unwrap_or_default();
27
28    if !matches!(
29        ext,
30        "rs" | "c" | "proto" | "toml" | "ts" | "tsx" | "js" | "css" | "html" | "py" | "ps1"
31    ) {
32        return Ok(());
33    }
34
35    let f = BufReader::new(File::open(path)?);
36    let mut lines = f.lines();
37    let (
38        allowed_non_copyright_first_line,
39        blank_after_allowed_non_copyright_first_line,
40        first_content_line,
41    ) = {
42        let line = lines.next().unwrap_or(Ok(String::new()))?;
43        // Someone may decide to put a script interpreter line (aka "shebang")
44        // in a .config or a .toml file, and mark the file as executable. While
45        // that's not common, we choose not to constrain creativity.
46        //
47        // The shebang (`#!`) is part of the valid grammar of Rust, and does not
48        // indicate that the file should be interpreted as a script. So we don't
49        // allow that line in Rust files.
50        //
51        // Some HTML files may start with a `<!DOCTYPE html>` line, so let that line pass as well
52        if (line.starts_with("#!") && ext != "rs")
53            || (line.starts_with("<!DOCTYPE html>") && ext == "html")
54        {
55            let allowed_non_copyright_first_line = line;
56            let after_allowed_non_copyright_first_line =
57                lines.next().unwrap_or(Ok(String::new()))?;
58            (
59                Some(allowed_non_copyright_first_line),
60                Some(after_allowed_non_copyright_first_line.is_empty()),
61                lines.next().unwrap_or(Ok(String::new()))?,
62            )
63        } else {
64            (None, None, line)
65        }
66    };
67    let second_content_line = lines.next().unwrap_or(Ok(String::new()))?;
68    let third_content_line = lines.next().unwrap_or(Ok(String::new()))?;
69
70    // Preserve any files which are copyright, but not by Microsoft.
71    if first_content_line.contains("Copyright") && !first_content_line.contains("Microsoft") {
72        return Ok(());
73    }
74
75    let mut missing_banner = !first_content_line.contains(HEADER_MIT_FIRST)
76        || !second_content_line.contains(HEADER_MIT_SECOND);
77    let mut missing_blank_line = !third_content_line.is_empty();
78    let mut header_lines = 2;
79
80    // TEMP: until we have more robust infrastructure for distinct
81    // microsoft-internal checks, include this "escape hatch" for preserving
82    // non-MIT licensed files when running `xtask fmt` in the msft internal
83    // repo. This uses a job-specific env var, instead of being properly plumbed
84    // through via `clap`, to make it easier to remove in the future.
85    let is_msft_internal = std::env::var("XTASK_FMT_COPYRIGHT_ALLOW_MISSING_MIT").is_ok();
86    if is_msft_internal && missing_banner {
87        // support both new and existing copyright banner styles
88        missing_banner =
89            !(first_content_line.contains("Copyright") && first_content_line.contains("Microsoft"));
90        missing_blank_line = !second_content_line.is_empty();
91        header_lines = 1;
92    }
93
94    if fix {
95        // windows gets touchy if you try and rename files while there are open
96        // file handles
97        drop(lines);
98
99        if missing_banner || missing_blank_line {
100            let path_fix = &{
101                let mut p = path.to_path_buf();
102                let ok = p.set_extension(format!("{}.fix", ext));
103                assert!(ok);
104                p
105            };
106
107            let mut f = BufReader::new(File::open(path)?);
108            let mut f_fixed = File::create(path_fix)?;
109
110            if let Some(allowed_non_copyright_first_line) = &allowed_non_copyright_first_line {
111                writeln!(f_fixed, "{allowed_non_copyright_first_line}")?;
112                f.read_line(&mut String::new())?;
113            }
114            if let Some(blank_after_allowed_non_copyright_first_line) =
115                blank_after_allowed_non_copyright_first_line
116            {
117                if !blank_after_allowed_non_copyright_first_line {
118                    writeln!(f_fixed)?;
119                }
120            }
121
122            if missing_banner {
123                let prefix = match ext {
124                    "rs" | "c" | "proto" | "ts" | "tsx" | "js" => "//",
125                    "toml" | "py" | "ps1" | "config" => "#",
126                    "css" => "/*",
127                    "html" => "<!--",
128                    _ => unreachable!(),
129                };
130
131                // Put a space here (if required), so that header lines without a prefix
132                // don't end with a trailing space. E.g. ` -->` instead of `-->`.
133                let suffix = match ext {
134                    "rs" | "c" | "proto" | "ts" | "tsx" | "js" | "toml" | "py" | "ps1"
135                    | "config" => "",
136                    "css" => " */",
137                    "html" => " -->",
138                    _ => unreachable!(),
139                };
140
141                // Preserve the UTF-8 BOM if it exists.
142                if allowed_non_copyright_first_line.is_none()
143                    && first_content_line.starts_with('\u{feff}')
144                {
145                    write!(f_fixed, "\u{feff}")?;
146                    // Skip the BOM.
147                    f.read_exact(&mut [0; 3])?;
148                }
149
150                writeln!(f_fixed, "{} {}{}", prefix, HEADER_MIT_FIRST, suffix)?;
151                if !is_msft_internal {
152                    writeln!(f_fixed, "{} {}{}", prefix, HEADER_MIT_SECOND, suffix)?;
153                }
154
155                writeln!(f_fixed)?; // also add that missing blank line
156            } else if missing_blank_line {
157                // copy the valid header from the current file
158                for _ in 0..header_lines {
159                    let mut s = String::new();
160                    f.read_line(&mut s)?;
161                    write!(f_fixed, "{}", s)?;
162                }
163
164                // ...but then tack on the blank newline as well
165                writeln!(f_fixed)?;
166            }
167
168            // copy over the rest of the file contents
169            std::io::copy(&mut f, &mut f_fixed)?;
170
171            // Windows gets touchy if you try and rename files while there are open
172            // file handles.
173            drop(f);
174            commit(f_fixed, path)?;
175        }
176    }
177
178    // Consider using an enum if there more than three,
179    // or the errors need to be compared.
180    let mut missing = vec![];
181    if missing_banner {
182        missing.push("the copyright & license header");
183    }
184    if missing_blank_line {
185        missing.push("a blank line after the copyright & license header");
186    }
187    if let Some(blank_after_allowed_non_copyright_first_line) =
188        blank_after_allowed_non_copyright_first_line
189    {
190        if !blank_after_allowed_non_copyright_first_line {
191            missing.push("a blank line after the script interpreter line");
192        }
193    }
194
195    if missing.is_empty() {
196        return Ok(());
197    }
198
199    if fix {
200        log::info!(
201            "applied fixes for missing {:?} in {}",
202            missing,
203            path.display()
204        );
205        Ok(())
206    } else {
207        Err(anyhow!("missing {:?} in {}", missing, path.display()))
208    }
209}