1use std::fs::File;
10use std::io::{self, Read};
11use std::path::{Path, PathBuf};
12
13use opys_core::{Artifact, HashEntry, Integrity, Source};
14use serde::{Deserialize, Serialize};
15use sha1::{Digest, Sha1};
16use sha2::Sha256;
17
18use crate::contribution::{BuildArtifact, Contribution};
19
20#[derive(Debug, thiserror::Error)]
21pub enum ScanError {
22 #[error("cannot read {path}: {source}")]
23 Io {
24 path: String,
25 #[source]
26 source: io::Error,
27 },
28}
29
30fn io_at(path: &Path) -> impl FnOnce(io::Error) -> ScanError + '_ {
31 move |source| ScanError::Io {
32 path: path.display().to_string(),
33 source,
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct ScannedFile {
40 pub rel: String,
42 pub dir: String,
44 pub filename: String,
46 pub abs: PathBuf,
48 pub size: u64,
49}
50
51fn walk(root: &Path, dir: &Path, out: &mut Vec<ScannedFile>) -> Result<(), ScanError> {
52 for entry in std::fs::read_dir(dir).map_err(io_at(dir))? {
53 let entry = entry.map_err(io_at(dir))?;
54 let abs = entry.path();
55 let kind = entry.file_type().map_err(io_at(&abs))?;
58 if kind.is_dir() {
59 walk(root, &abs, out)?;
60 } else if kind.is_file() {
61 let size = entry.metadata().map_err(io_at(&abs))?.len();
62 let parts: Vec<String> = abs
63 .strip_prefix(root)
64 .unwrap_or(&abs)
65 .components()
66 .map(|part| part.as_os_str().to_string_lossy().into_owned())
67 .collect();
68 let (filename, dirs) = parts.split_last().expect("a file has a name");
69 out.push(ScannedFile {
70 rel: parts.join("/"),
71 dir: dirs.join("/"),
72 filename: filename.clone(),
73 abs,
74 size,
75 });
76 }
77 }
78 Ok(())
79}
80
81pub fn scan_directory(root: &Path) -> Result<Vec<ScannedFile>, ScanError> {
87 let mut files = Vec::new();
88 walk(root, root, &mut files)?;
89 files.sort_by(|a, b| a.rel.cmp(&b.rel));
90 Ok(files)
91}
92
93#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
95#[serde(rename_all = "lowercase")]
96pub enum ScanHash {
97 #[default]
98 Sha1,
99 Sha256,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
104#[serde(deny_unknown_fields)]
105pub struct PlacedFile {
106 pub abs: PathBuf,
108 pub path: String,
110 #[serde(default)]
114 pub url: Option<String>,
115}
116
117fn digest<D: Digest>(path: &Path) -> Result<(String, u64), ScanError> {
119 let mut file = File::open(path).map_err(io_at(path))?;
120 let mut hasher = D::new();
121 let mut buffer = [0u8; 64 * 1024];
122 let mut size = 0u64;
123 loop {
124 let read = file.read(&mut buffer).map_err(io_at(path))?;
125 if read == 0 {
126 return Ok((hex::encode(hasher.finalize()), size));
127 }
128 hasher.update(&buffer[..read]);
129 size += read as u64;
130 }
131}
132
133pub fn scanned_files(files: &[PlacedFile], hash: ScanHash) -> Result<Contribution, ScanError> {
140 let mut contribution = Contribution::default();
141 for file in files {
142 let artifact = match &file.url {
143 None => BuildArtifact::file(&file.path, &file.abs),
146 Some(url) => {
147 let (entry, size) = match hash {
148 ScanHash::Sha1 => {
149 let (sha1, size) = digest::<Sha1>(&file.abs)?;
150 (HashEntry::Sha1 { sha1 }, size)
151 }
152 ScanHash::Sha256 => {
153 let (sha256, size) = digest::<Sha256>(&file.abs)?;
154 (HashEntry::Sha256 { sha256 }, size)
155 }
156 };
157 Artifact {
158 path: file.path.clone(),
159 source: Source::Url { url: url.clone() },
160 size: Some(size),
161 rules: Vec::new(),
162 integrity: Some(Integrity::One(entry)),
163 metadata: None,
164 extract: None,
165 }
166 .into()
167 }
168 };
169 contribution.artifacts.push(artifact);
170 }
171 Ok(contribution)
172}