1use anyhow::Result;
10use lazy_static::lazy_static;
11
12lazy_static! {
13 static ref HEADER_DEFINITIONS: Vec<HeaderDefinition<'static>> = vec![
15 HeaderDefinition {
16 extensions: vec![".c", ".h", ".gv", ".java", ".scala", ".kt", ".kts"],
17 header_prefix: HeaderPrefix::new("/*", " * ", " */"),
18 },
19 HeaderDefinition {
20 extensions: vec![
21 ".js", ".mjs", ".cjs", ".jsx", ".tsx", ".css", ".scss", ".sass", ".ts",
22 ],
23 header_prefix: HeaderPrefix::new("/**", " * ", " */"),
24 },
25 HeaderDefinition {
26 extensions: vec![
27 ".cc", ".cpp", ".cs", ".go", ".hcl", ".hh", ".hpp", ".m", ".mm", ".proto", ".rs",
28 ".swift", ".dart", ".groovy", ".v", ".sv", ".php",
29 ],
30 header_prefix: HeaderPrefix::new("", "// ", ""),
31 },
32 HeaderDefinition {
33 extensions: vec![
34 ".py",
35 ".sh",
36 ".yaml",
37 ".yml",
38 ".dockerfile",
39 "dockerfile",
40 ".rb",
41 "gemfile",
42 ".tcl",
43 ".tf",
44 ".bzl",
45 ".pl",
46 ".pp",
47 "build",
48 ".build",
49 ".toml",
50 ],
51 header_prefix: HeaderPrefix::new("", "# ", ""),
52 },
53 HeaderDefinition {
54 extensions: vec![".el", ".lisp"],
55 header_prefix: HeaderPrefix::new("", ";; ", ""),
56 },
57 HeaderDefinition {
58 extensions: vec![".erl"],
59 header_prefix: HeaderPrefix::new("", "% ", ""),
60 },
61 HeaderDefinition {
62 extensions: vec![".hs", ".sql", ".sdl"],
63 header_prefix: HeaderPrefix::new("", "-- ", ""),
64 },
65 HeaderDefinition {
66 extensions: vec![".html", ".xml", ".vue", ".wxi", ".wxl", ".wxs"],
67 header_prefix: HeaderPrefix::new("<!--", " ", "-->"),
68 },
69 HeaderDefinition {
70 extensions: vec![".j2"],
71 header_prefix: HeaderPrefix::new("{#", "", "#}"),
72 },
73 HeaderDefinition {
74 extensions: vec![".ml", ".mli", ".mll", ".mly"],
75 header_prefix: HeaderPrefix::new("(**", " ", "*)"),
76 },
77 ];
79}
80
81const HEAD: &[&str] = &[
82 "#!",
84 "<?xml",
86 "<!doctype",
88 "# encoding:",
90 "# frozen_string_literal:",
92 "<?php",
94 "# escape",
96 "# syntax",
97];
98
99pub struct SourceHeaders;
101
102impl SourceHeaders {
103 pub fn find_header_definition_by_extension<'a, E: AsRef<str>>(
105 extension: E,
106 ) -> Option<&'a HeaderDefinition<'a>> {
107 HEADER_DEFINITIONS
108 .iter()
109 .find(|source| source.contains_extension(Some(&extension)))
110 }
111
112 pub fn find_header_prefix_for_extension<'a, E: AsRef<str>>(
114 extension: E,
115 ) -> Option<&'a HeaderPrefix<'a>> {
116 SourceHeaders::find_header_definition_by_extension(&extension)
117 .map(|source| &source.header_prefix)
118 }
119}
120
121pub struct HeaderDefinition<'a> {
123 pub extensions: Vec<&'a str>,
125 pub header_prefix: HeaderPrefix<'a>,
127}
128
129impl HeaderDefinition<'_> {
130 pub fn contains_extension<E: AsRef<str>>(&self, extension: Option<E>) -> bool {
132 extension
133 .map_or(false, |e| self.extensions.contains(&e.as_ref()))
134 .to_owned()
135 }
136}
137
138#[derive(Debug, Clone)]
140pub struct HeaderPrefix<'a> {
141 pub top: &'a str,
143 pub mid: &'a str,
145 pub bottom: &'a str,
147}
148
149impl<'a> HeaderPrefix<'a> {
150 pub fn apply<T>(&self, template: T) -> Result<String>
153 where
154 T: AsRef<str>,
155 {
156 let Self { bottom, mid, top } = &self;
157
158 let mut out = String::new();
159 if !top.is_empty() {
160 out.push_str(top);
161 out.push('\n');
162 }
163
164 let lines = template.as_ref().lines();
165 for line in lines {
166 out.push_str(mid);
167 out.push_str(line.trim_end_matches(char::is_whitespace));
168 out.push('\n');
169 }
170
171 if !bottom.is_empty() {
172 out.push_str(bottom);
173 out.push('\n');
174 }
175
176 out.push('\n');
177
178 Ok(out)
179 }
180
181 pub fn new(top: &'a str, mid: &'a str, bottom: &'a str) -> HeaderPrefix<'a> {
183 HeaderPrefix { top, mid, bottom }
184 }
185}
186
187pub fn extract_hash_bang(b: &[u8]) -> Option<Vec<u8>> {
194 let mut line = Vec::new();
195
196 for &c in b {
197 line.push(c);
198 if c == b'\n' {
199 break;
200 }
201 }
202
203 let first = String::from_utf8_lossy(&line).to_lowercase();
204
205 for &h in HEAD {
206 if first.starts_with(h) {
207 return Some(line);
208 }
209 }
210
211 None
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::template::copyright::{SpdxCopyrightNotice, SPDX_COPYRIGHT_NOTICE};
218
219 #[test]
220 fn test_execute_template_spdx_copyright_notice() {
221 let rs_header_prefix = SourceHeaders::find_header_prefix_for_extension(".rs").unwrap();
222 let reg = handlebars::Handlebars::new();
223
224 let data = SpdxCopyrightNotice {
226 year: Some(2022),
227 owner: "Bilbo Baggins".to_string(),
228 license: "MIT".to_string(),
229 };
230
231 let template = reg.render_template(SPDX_COPYRIGHT_NOTICE, &data);
232 assert!(template.is_ok());
233 let template = template.unwrap();
234
235 let result = rs_header_prefix.apply(&template).unwrap();
236 let expected: &str = r#"// Copyright 2022 Bilbo Baggins
237// SPDX-License-Identifier: MIT
238
239"#;
240 assert_eq!(&result, expected);
241
242 let empty_template = "";
244 let result = rs_header_prefix.apply(empty_template).unwrap();
245 let expected = "\n";
246 assert_eq!(&result, expected);
247
248 let js_header_prefix = SourceHeaders::find_header_prefix_for_extension(".js").unwrap();
250 let result = js_header_prefix.apply(template).unwrap();
251
252 #[deny(clippy::all)]
254 let expected: &str = r#"/**
255 * Copyright 2022 Bilbo Baggins
256 * SPDX-License-Identifier: MIT
257 */
258
259"#;
260 assert_eq!(&result, expected);
261 }
262
263 #[test]
264 fn test_hash_bang_with_valid_prefix() {
265 let input = "#!/bin/bash\nrest of the script".as_bytes();
267 let result = extract_hash_bang(input);
268 let expected = Some(b"#!/bin/bash\n".to_vec());
269 assert_eq!(result, expected);
270 }
271
272 #[test]
273 fn test_hash_bang_with_invalid_prefix() {
274 let input = "Invalid hash-bang line\nrest of the script".as_bytes();
276 let result = extract_hash_bang(input);
277 let expected = None;
278 assert_eq!(result, expected);
279 }
280
281 #[test]
282 fn test_hash_bang_with_multiple_valid_prefixes() {
283 let input = "<?xml\nrest of the content".as_bytes();
285 let result = extract_hash_bang(input);
286 let expected = Some(b"<?xml\n".to_vec());
287 assert_eq!(result, expected);
288 }
289
290 #[test]
291 fn test_hash_bang_with_empty_input() {
292 let input = "".as_bytes();
294 let result = extract_hash_bang(input);
295 let expected = None;
296 assert_eq!(result, expected);
297 }
298
299 #[test]
300 fn test_hash_bang_with_partial_line() {
301 let input = "#!/usr/bin/env python".as_bytes();
303 let result = extract_hash_bang(input);
304 let expected = Some("#!/usr/bin/env python".as_bytes().to_vec());
305 assert_eq!(result, expected);
306 }
307}