1use crate::error::Result;
4use md5::{Digest as Md5Digest, Md5};
5use sha1::Sha1;
6use sha2::{Sha256, Sha512};
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone, Copy)]
11pub enum HashAlgorithm {
12 Md5,
13 Sha1,
14 Sha256,
15 Sha512,
16}
17
18#[derive(Debug, Clone)]
20pub struct HashSpec {
21 pub algo: HashAlgorithm,
22 pub value: String,
23}
24
25impl HashSpec {
26 pub fn md5(value: &str) -> Self {
27 Self {
28 algo: HashAlgorithm::Md5,
29 value: value.to_lowercase(),
30 }
31 }
32
33 pub fn sha1(value: &str) -> Self {
34 Self {
35 algo: HashAlgorithm::Sha1,
36 value: value.to_lowercase(),
37 }
38 }
39
40 pub fn sha256(value: &str) -> Self {
41 Self {
42 algo: HashAlgorithm::Sha256,
43 value: value.to_lowercase(),
44 }
45 }
46
47 pub fn sha512(value: &str) -> Self {
48 Self {
49 algo: HashAlgorithm::Sha512,
50 value: value.to_lowercase(),
51 }
52 }
53}
54
55pub struct Cache {
57 root: PathBuf,
58}
59
60impl Cache {
61 pub fn new() -> Result<Self> {
63 let root = dirs::cache_dir()
64 .unwrap_or_else(|| PathBuf::from("~/.cache/gdown"))
65 .join("gdown");
66 Ok(Self { root })
67 }
68
69 pub fn with_root(root: PathBuf) -> Self {
71 Self { root }
72 }
73
74 fn cache_path(&self, url: &str) -> PathBuf {
76 let hash = Sha256::digest(url.as_bytes());
77 let hash_str = format!("{:x}", hash);
78 self.root.join(&hash_str)
79 }
80
81 pub fn is_cached(&self, url: &str) -> bool {
83 let path = self.cache_path(url);
84 path.exists()
85 }
86
87 pub fn compute_hash_sync(&self, path: &Path, algo: HashAlgorithm) -> Result<String> {
89 use std::io::Read;
90
91 let mut file = std::fs::File::open(path)?;
92 let mut buffer = vec![0u8; 8192];
93
94 match algo {
95 HashAlgorithm::Md5 => {
96 let mut hasher = Md5::new();
97 loop {
98 let bytes_read = file.read(&mut buffer)?;
99 if bytes_read == 0 {
100 break;
101 }
102 hasher.update(&buffer[..bytes_read]);
103 }
104 Ok(format!("{:x}", hasher.finalize()))
105 }
106 HashAlgorithm::Sha1 => {
107 let mut hasher = Sha1::new();
108 loop {
109 let bytes_read = file.read(&mut buffer)?;
110 if bytes_read == 0 {
111 break;
112 }
113 hasher.update(&buffer[..bytes_read]);
114 }
115 Ok(format!("{:x}", hasher.finalize()))
116 }
117 HashAlgorithm::Sha256 => {
118 let mut hasher = Sha256::new();
119 loop {
120 let bytes_read = file.read(&mut buffer)?;
121 if bytes_read == 0 {
122 break;
123 }
124 hasher.update(&buffer[..bytes_read]);
125 }
126 Ok(format!("{:x}", hasher.finalize()))
127 }
128 HashAlgorithm::Sha512 => {
129 let mut hasher = Sha512::new();
130 loop {
131 let bytes_read = file.read(&mut buffer)?;
132 if bytes_read == 0 {
133 break;
134 }
135 hasher.update(&buffer[..bytes_read]);
136 }
137 Ok(format!("{:x}", hasher.finalize()))
138 }
139 }
140 }
141
142 pub fn verify_hash(&self, path: &Path, spec: &HashSpec) -> Result<bool> {
144 let computed = self.compute_hash_sync(path, spec.algo)?;
145 Ok(computed.to_lowercase() == spec.value.to_lowercase())
146 }
147}
148
149impl Default for Cache {
150 fn default() -> Self {
151 Self::new().expect("Failed to create cache directory")
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_hash_spec_md5() {
161 let spec = HashSpec::md5("abc123");
162 assert!(matches!(spec.algo, HashAlgorithm::Md5));
163 assert_eq!(spec.value, "abc123");
164 }
165
166 #[test]
167 fn test_hash_spec_sha256() {
168 let spec = HashSpec::sha256("def456");
169 assert!(matches!(spec.algo, HashAlgorithm::Sha256));
170 assert_eq!(spec.value, "def456");
171 }
172}