licensa/template/
header.rs

1// Copyright 2024 Nelson Dominguez
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! # Source Headers
5//!
6//! The `SourceHeaders` struct provides methods for finding header definitions and prefixes based on file extensions.
7//! It contains a predefined list of `SourceHeaderDefinition` instances.
8
9use anyhow::Result;
10use lazy_static::lazy_static;
11
12lazy_static! {
13  /// Represents a predefined list of source header definitions.
14  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    // TODO: 	handle cmake files
78  ];
79}
80
81const HEAD: &[&str] = &[
82    // shell script
83    "#!",
84    // XML declaratioon
85    "<?xml",
86    // HTML doctype
87    "<!doctype",
88    // Ruby encoding
89    "# encoding:",
90    // Ruby interpreter instruction
91    "# frozen_string_literal:",
92    // PHP opening tag
93    "<?php",
94    // Dockerfile directive https://docs.docker.com/engine/reference/builder/#parser-directives
95    "# escape",
96    "# syntax",
97];
98
99/// Represents a utility for working with source headers.
100pub struct SourceHeaders;
101
102impl SourceHeaders {
103    /// Finds the header definition based on the given file extension.
104    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    /// Finds the header prefix based on the given file extension.
113    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
121/// Represents a source header definition with a list of file extensions and a corresponding prefix.
122pub struct HeaderDefinition<'a> {
123    /// List of file extensions associated with the header definition.
124    pub extensions: Vec<&'a str>,
125    /// Corresponding source header prefix.
126    pub header_prefix: HeaderPrefix<'a>,
127}
128
129impl HeaderDefinition<'_> {
130    /// Checks if the given extension is contained in the list of file extensions.
131    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/// Represents the prefix structure for a source header.
139#[derive(Debug, Clone)]
140pub struct HeaderPrefix<'a> {
141    /// Top part of the header.
142    pub top: &'a str,
143    /// Middle part of the header.
144    pub mid: &'a str,
145    /// Bottom part of the header.
146    pub bottom: &'a str,
147}
148
149impl<'a> HeaderPrefix<'a> {
150    // execute_template will execute a license template t with data d
151    // and prefix the result with top, middle and bottom.
152    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    /// Creates a new `SourceHeaderPrefix` instance with the specified top, mid, and bottom parts.
182    pub fn new(top: &'a str, mid: &'a str, bottom: &'a str) -> HeaderPrefix<'a> {
183        HeaderPrefix { top, mid, bottom }
184    }
185}
186
187/// Extracts the hash-bang line from the given byte slice.
188///
189/// The hash-bang line is the first line in the slice ending with a newline character.
190/// It checks if the lowercase hash-bang line starts with any of the specified prefixes.
191///
192/// Returns the hash-bang line if a matching prefix is found, otherwise returns `None`.
193pub 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        // Test case 1
225        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        // Empty template and prefix
243        let empty_template = "";
244        let result = rs_header_prefix.apply(empty_template).unwrap();
245        let expected = "\n";
246        assert_eq!(&result, expected);
247
248        // JavaScript
249        let js_header_prefix = SourceHeaders::find_header_prefix_for_extension(".js").unwrap();
250        let result = js_header_prefix.apply(template).unwrap();
251
252        // Disable linting for template whitespace to be valid
253        #[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        // Test with a valid hash-bang line
266        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        // Test with an invalid hash-bang line
275        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        // Test with multiple valid hash-bang prefixes
284        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        // Test with an empty input
293        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        // Test with a partial line (no newline character)
302        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}