Skip to main content

provenant/utils/file/
path.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Path-oriented helpers: filesystem metadata, glob exclusion, and
5//! extension/filename predicates used across the file-classification utilities.
6
7use std::fs;
8use std::path::Path;
9
10use chrono::{TimeZone, Utc};
11use glob::Pattern;
12
13pub(super) const PLAIN_TEXT_EXTENSIONS: &[&str] = &[
14    "rst", "rest", "md", "txt", "log", "json", "xml", "yaml", "yml", "toml", "ini",
15];
16
17/// Get the last modified date of a file as a `YYYY-MM-DD` string.
18pub fn get_creation_date(metadata: &fs::Metadata) -> Option<String> {
19    let time = metadata.modified().ok()?;
20    let seconds_since_epoch = time.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs() as i64;
21
22    Some(
23        Utc.timestamp_opt(seconds_since_epoch, 0)
24            .single()
25            .unwrap_or_else(Utc::now)
26            .format("%Y-%m-%d")
27            .to_string(),
28    )
29}
30
31/// Check if a path should be excluded based on a list of glob patterns.
32pub fn is_path_excluded(path: &Path, exclude_patterns: &[Pattern]) -> bool {
33    let path_str = path.to_string_lossy();
34    let file_name = path
35        .file_name()
36        .map(|name| name.to_string_lossy())
37        .unwrap_or_default();
38
39    for pattern in exclude_patterns {
40        // Match against full path
41        if pattern.matches(&path_str) {
42            return true;
43        }
44
45        // Match against just the file/directory name
46        if pattern.matches(&file_name) {
47            return true;
48        }
49    }
50
51    false
52}
53
54pub(super) fn extension(path: &Path) -> Option<&str> {
55    path.extension().and_then(|ext| ext.to_str())
56}
57
58pub(super) fn lower_extension(path: &Path) -> Option<String> {
59    extension(path).map(|ext| ext.to_ascii_lowercase())
60}
61
62pub(super) fn lower_file_name(path: &Path) -> String {
63    path.file_name()
64        .and_then(|name| name.to_str())
65        .map(|name| name.to_ascii_lowercase())
66        .unwrap_or_default()
67}
68
69pub(super) fn is_plain_text(path: &Path) -> bool {
70    lower_extension(path)
71        .as_deref()
72        .is_some_and(|ext| PLAIN_TEXT_EXTENSIONS.contains(&ext))
73}
74
75pub(super) fn is_makefile(path: &Path) -> bool {
76    matches!(lower_file_name(path).as_str(), "makefile" | "makefile.inc")
77}
78
79pub(super) fn is_source_map(path: &Path) -> bool {
80    let path_lower = path.to_string_lossy().to_ascii_lowercase();
81    path_lower.ends_with(".js.map") || path_lower.ends_with(".css.map")
82}
83
84pub(super) fn is_c_like_source(path: &Path) -> bool {
85    lower_extension(path).as_deref().is_some_and(|ext| {
86        matches!(
87            ext,
88            "c" | "cc"
89                | "cp"
90                | "cpp"
91                | "cxx"
92                | "c++"
93                | "h"
94                | "hh"
95                | "hpp"
96                | "hxx"
97                | "h++"
98                | "i"
99                | "ii"
100                | "m"
101                | "s"
102                | "asm"
103        )
104    })
105}
106
107pub(super) fn is_java_like_source(path: &Path) -> bool {
108    lower_extension(path)
109        .as_deref()
110        .is_some_and(|ext| matches!(ext, "java" | "aj" | "jad" | "ajt"))
111}