1use std::{
4 collections::HashSet,
5 fmt::Display,
6 fs::{metadata, read_dir},
7 path::Path,
8};
9
10use alpm_types::{MetadataFileName, PKGBUILD_FILE_NAME, SRCINFO_FILE_NAME};
11#[cfg(feature = "cli")]
12use clap::ValueEnum;
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15use strum::{Display as StrumDisplay, VariantArray};
16
17use crate::Error;
18
19#[derive(Clone, Debug, PartialEq)]
33pub struct ScopedName {
34 scope: LintScope,
35 name: &'static str,
36}
37
38impl ScopedName {
39 pub fn new(scope: LintScope, name: &'static str) -> Self {
41 Self { scope, name }
42 }
43}
44
45impl Display for ScopedName {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 write!(f, "{}::{}", self.scope, self.name)
48 }
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, StrumDisplay, VariantArray)]
58#[cfg_attr(feature = "cli", derive(ValueEnum))]
59#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
60#[strum(serialize_all = "snake_case")]
61pub enum LintScope {
62 SourceRepository,
73 Package,
84 BuildInfo,
88 PackageBuild,
92 PackageInfo,
96 SourceInfo,
100}
101
102impl LintScope {
103 pub fn contains(&self, other: &LintScope) -> bool {
121 match self {
122 LintScope::SourceRepository => match other {
124 LintScope::SourceRepository | LintScope::SourceInfo | LintScope::PackageBuild => {
125 true
126 }
127 LintScope::BuildInfo | LintScope::PackageInfo | LintScope::Package => false,
128 },
129 LintScope::Package => match other {
131 LintScope::Package | LintScope::PackageBuild | LintScope::PackageInfo => true,
132 LintScope::BuildInfo | LintScope::SourceRepository | LintScope::SourceInfo => false,
133 },
134 LintScope::BuildInfo
136 | LintScope::PackageBuild
137 | LintScope::PackageInfo
138 | LintScope::SourceInfo => self == other,
139 }
140 }
141
142 pub fn detect(path: &Path) -> Result<LintScope, Error> {
155 let metadata = metadata(path).map_err(|source| Error::IoPath {
157 path: path.to_owned(),
158 context: "getting metadata of path",
159 source,
160 })?;
161
162 if metadata.is_file() {
164 let filename = path.file_name().ok_or(Error::NoLintScope {
165 path: path.to_owned(),
166 })?;
167
168 if filename == alpm_types::PKGBUILD_FILE_NAME {
170 return Ok(LintScope::PackageBuild);
171 } else if filename == alpm_types::SRCINFO_FILE_NAME {
172 return Ok(LintScope::SourceInfo);
173 } else if filename == Into::<&'static str>::into(MetadataFileName::BuildInfo) {
175 return Ok(LintScope::BuildInfo);
176 } else if filename == Into::<&'static str>::into(MetadataFileName::PackageInfo) {
177 return Ok(LintScope::PackageInfo);
178 } else {
179 return Err(Error::NoLintScope {
180 path: path.to_path_buf(),
181 });
182 }
183 }
184
185 let entries = read_dir(path).map_err(|source| Error::IoPath {
189 path: path.to_owned(),
190 context: "read directory entries",
191 source,
192 })?;
193
194 let mut filenames = HashSet::new();
195
196 for entry in entries {
199 let entry = entry.map_err(|source| Error::IoPath {
200 path: path.to_owned(),
201 context: "read a specific directory entries",
202 source,
203 })?;
204 let entry_path = entry.path();
205 let metadata = entry.metadata().map_err(|source| Error::IoPath {
206 path: entry_path.to_owned(),
207 context: "getting metadata of file",
208 source,
209 })?;
210
211 if !metadata.is_file() {
213 continue;
214 }
215
216 let Some(filename) = entry_path.file_name() else {
217 continue;
218 };
219 filenames.insert(filename.to_string_lossy().to_string());
220 }
221
222 if filenames.contains(PKGBUILD_FILE_NAME) && filenames.contains(SRCINFO_FILE_NAME) {
223 Ok(LintScope::SourceRepository)
224 } else if filenames.contains(MetadataFileName::BuildInfo.into())
225 && filenames.contains(MetadataFileName::PackageInfo.into())
226 {
227 Ok(LintScope::Package)
228 } else if filenames.contains(PKGBUILD_FILE_NAME) {
229 Ok(LintScope::PackageBuild)
230 } else if filenames.contains(SRCINFO_FILE_NAME) {
231 Ok(LintScope::SourceInfo)
232 } else if filenames.contains(MetadataFileName::BuildInfo.into()) {
233 Ok(LintScope::BuildInfo)
234 } else if filenames.contains(MetadataFileName::PackageInfo.into()) {
235 Ok(LintScope::PackageInfo)
236 } else {
237 Err(Error::NoLintScope {
238 path: path.to_path_buf(),
239 })
240 }
241 }
242
243 pub fn is_single_file(&self) -> bool {
245 match self {
246 LintScope::SourceRepository | LintScope::Package => false,
247 LintScope::BuildInfo
248 | LintScope::PackageBuild
249 | LintScope::PackageInfo
250 | LintScope::SourceInfo => true,
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use std::fs::File;
258
259 use rstest::rstest;
260 use testresult::TestResult;
261
262 use super::*;
263
264 #[rstest]
266 #[case::package(vec!["PKGBUILD", ".SRCINFO"], LintScope::SourceRepository)]
267 #[case::package_with_other_files(vec!["test_file", "PKGBUILD", ".SRCINFO", ".BUILDINFO", ".PKGINFO"], LintScope::SourceRepository)]
268 #[case::package_build(vec!["PKGBUILD"], LintScope::PackageBuild)]
269 #[case::source_info(vec![".SRCINFO"], LintScope::SourceInfo)]
270 #[case::source_repo(vec![".BUILDINFO", ".PKGINFO"], LintScope::Package)]
271 #[case::source_repo_with_other_files(vec!["test_file", "PKGBUILD", ".BUILDINFO", ".PKGINFO"], LintScope::Package)]
272 #[case::build_info(vec![".BUILDINFO"], LintScope::BuildInfo)]
273 #[case::package_info(vec![".PKGINFO"], LintScope::PackageInfo)]
274 fn detect_scope_in_directory(
275 #[case] files: Vec<&'static str>,
276 #[case] expected: LintScope,
277 ) -> TestResult<()> {
278 let tmp_dir = tempfile::tempdir()?;
280
281 for name in &files {
283 let path = tmp_dir.path().join(name);
284 File::create(&path)?;
285 }
286
287 let scope = LintScope::detect(tmp_dir.path())?;
288
289 assert_eq!(
290 scope, expected,
291 "Expected '{expected}' scope for file set {files:?}"
292 );
293
294 Ok(())
295 }
296
297 #[rstest]
299 #[case::unknown_files(vec!["test_file", "test_file2"])]
300 #[case::no_files(vec![])]
301 fn fail_to_detect_scope_in_directory(#[case] files: Vec<&'static str>) -> TestResult<()> {
302 let tmp_dir = tempfile::tempdir()?;
304
305 for name in &files {
307 let path = tmp_dir.path().join(name);
308 File::create(&path)?;
309 }
310
311 let error = match LintScope::detect(tmp_dir.path()) {
312 Ok(scope) => {
313 panic!("Expected an error for scope detection for file set {files:?}, got {scope}");
314 }
315 Err(err) => err,
316 };
317
318 assert!(
319 matches!(error, Error::NoLintScope { .. }),
320 "Expected 'NoLintScope' error for file set {files:?}"
321 );
322
323 Ok(())
324 }
325
326 #[rstest]
328 #[case::package_build("PKGBUILD", LintScope::PackageBuild)]
329 #[case::source_info(".SRCINFO", LintScope::SourceInfo)]
330 #[case::build_info(".BUILDINFO", LintScope::BuildInfo)]
331 #[case::package_info(".PKGINFO", LintScope::PackageInfo)]
332 fn detect_scope_of_file(
333 #[case] file: &'static str,
334 #[case] expected: LintScope,
335 ) -> TestResult<()> {
336 let tmp_dir = tempfile::tempdir()?;
338
339 let path = tmp_dir.path().join(file);
341 File::create(&path)?;
342
343 let scope = LintScope::detect(&path)?;
344
345 assert_eq!(
346 scope, expected,
347 "Expected '{expected}' scope for file {file:?}"
348 );
349 assert!(scope.is_single_file());
350
351 Ok(())
352 }
353}