1use std::io::Write;
8use std::path::Path;
9
10use sha2::{Digest, Sha256};
11use sui_compat::nar::NarWriter;
12use sui_compat::narinfo::NarInfo;
13
14use crate::signing::CacheSigner;
15use crate::storage::StorageBackend;
16use crate::CacheError;
17
18#[derive(Debug, Clone)]
20pub struct PushResult {
21 pub hash: String,
23 pub compressed_size: u64,
25 pub nar_size: u64,
27}
28
29pub async fn push_path(
44 storage: &dyn StorageBackend,
45 signer: &CacheSigner,
46 store_path: &str,
47 hash: &str,
48 references: &[String],
49 deriver: Option<&str>,
50) -> Result<PushResult, CacheError> {
51 let path = Path::new(store_path);
52 if !path.exists() {
53 return Err(CacheError::PathNotFound(store_path.to_string()));
54 }
55
56 let nar_data = dump_path_to_nar(path)?;
58
59 let nar_hash = sha256_hex(&nar_data);
61 let nar_size = nar_data.len() as u64;
62
63 let compressed = compress_xz(&nar_data)?;
65 let compressed_size = compressed.len() as u64;
66
67 let file_hash = sha256_hex(&compressed);
69
70 let nar_url = format!("nar/{hash}.nar.xz");
72 let narinfo = NarInfo {
73 store_path: store_path.to_string(),
74 url: nar_url.clone(),
75 compression: "xz".to_string(),
76 file_hash: format!("sha256:{file_hash}"),
77 file_size: compressed_size,
78 nar_hash: format!("sha256:{nar_hash}"),
79 nar_size,
80 references: references.to_vec(),
81 deriver: deriver.map(String::from),
82 signatures: vec![],
83 ca: None,
84 };
85
86 let sig = signer.sign_narinfo(&narinfo);
88 let narinfo = NarInfo {
89 signatures: vec![sig],
90 ..narinfo
91 };
92
93 storage.put_nar(&nar_url, &compressed).await?;
95 storage.put_narinfo(hash, &narinfo.serialize()).await?;
96
97 Ok(PushResult {
98 hash: hash.to_string(),
99 compressed_size,
100 nar_size,
101 })
102}
103
104fn dump_path_to_nar(path: &Path) -> Result<Vec<u8>, CacheError> {
106 let mut buf = Vec::new();
107 NarWriter::write_path(&mut buf, path).map_err(|e| {
108 CacheError::Io(std::io::Error::new(
109 std::io::ErrorKind::Other,
110 format!("NAR dump failed: {e}"),
111 ))
112 })?;
113 Ok(buf)
114}
115
116fn compress_xz(data: &[u8]) -> Result<Vec<u8>, CacheError> {
118 let mut compressed = Vec::new();
119 let mut encoder = xz2::write::XzEncoder::new(&mut compressed, 6);
120 encoder.write_all(data).map_err(CacheError::Io)?;
121 encoder.finish().map_err(CacheError::Io)?;
122 Ok(compressed)
123}
124
125fn sha256_hex(data: &[u8]) -> String {
127 let digest = Sha256::digest(data);
128 let mut s = String::with_capacity(64);
129 for b in digest.as_slice() {
130 use std::fmt::Write;
131 let _ = write!(s, "{b:02x}");
132 }
133 s
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::signing::CacheSigner;
140 use crate::storage::local::LocalStorage;
141
142 #[tokio::test]
143 async fn push_single_file() {
144 let cache_dir = tempfile::tempdir().unwrap();
145 let storage = LocalStorage::new(cache_dir.path());
146 let signer = CacheSigner::generate("test-cache".to_string());
147
148 let store_dir = tempfile::tempdir().unwrap();
150 let fake_store = store_dir.path().join("nix/store/abc-hello-1.0");
151 std::fs::create_dir_all(&fake_store).unwrap();
152 std::fs::write(fake_store.join("hello.txt"), b"Hello world!").unwrap();
153
154 let result = push_path(
155 &storage,
156 &signer,
157 fake_store.to_str().unwrap(),
158 "abc",
159 &[],
160 None,
161 )
162 .await
163 .unwrap();
164
165 assert_eq!(result.hash, "abc");
166 assert!(result.nar_size > 0);
167 assert!(result.compressed_size > 0);
168
169 let narinfo = storage.get_narinfo("abc").await.unwrap().unwrap();
171 let parsed = NarInfo::parse(&narinfo).unwrap();
172 assert_eq!(parsed.compression, "xz");
173 assert_eq!(parsed.signatures.len(), 1);
174 assert!(parsed.signatures[0].starts_with("test-cache:"));
175
176 let nar = storage.get_nar("nar/abc.nar.xz").await.unwrap().unwrap();
178 assert!(!nar.is_empty());
179 }
180
181 #[tokio::test]
182 async fn push_nonexistent_path_errors() {
183 let dir = tempfile::tempdir().unwrap();
184 let storage = LocalStorage::new(dir.path());
185 let signer = CacheSigner::generate("k".to_string());
186
187 let result = push_path(
188 &storage,
189 &signer,
190 "/nix/store/does-not-exist-12345",
191 "nope",
192 &[],
193 None,
194 )
195 .await;
196
197 assert!(result.is_err());
198 assert!(matches!(result, Err(CacheError::PathNotFound(_))));
199 }
200
201 #[tokio::test]
202 async fn push_with_references() {
203 let cache_dir = tempfile::tempdir().unwrap();
204 let storage = LocalStorage::new(cache_dir.path());
205 let signer = CacheSigner::generate("k".to_string());
206
207 let store_dir = tempfile::tempdir().unwrap();
208 let path = store_dir.path().join("pkg");
209 std::fs::create_dir_all(&path).unwrap();
210 std::fs::write(path.join("file"), b"data").unwrap();
211
212 let refs = vec!["dep1-glibc".to_string(), "dep2-gcc".to_string()];
213 let result = push_path(
214 &storage,
215 &signer,
216 path.to_str().unwrap(),
217 "xyz",
218 &refs,
219 Some("builder.drv"),
220 )
221 .await
222 .unwrap();
223
224 assert_eq!(result.hash, "xyz");
225
226 let narinfo = storage.get_narinfo("xyz").await.unwrap().unwrap();
227 let parsed = NarInfo::parse(&narinfo).unwrap();
228 assert_eq!(parsed.references, refs);
229 assert_eq!(parsed.deriver, Some("builder.drv".to_string()));
230 }
231
232 #[tokio::test]
233 async fn pushed_narinfo_is_valid_and_verifiable() {
234 let cache_dir = tempfile::tempdir().unwrap();
235 let storage = LocalStorage::new(cache_dir.path());
236 let signer = CacheSigner::generate("verify-key".to_string());
237 let pk_str = signer.public_key_string();
238
239 let store_dir = tempfile::tempdir().unwrap();
240 let path = store_dir.path().join("test-pkg");
241 std::fs::create_dir_all(&path).unwrap();
242 std::fs::write(path.join("data"), b"test content").unwrap();
243
244 push_path(
245 &storage,
246 &signer,
247 path.to_str().unwrap(),
248 "ttt",
249 &[],
250 None,
251 )
252 .await
253 .unwrap();
254
255 let narinfo_text = storage.get_narinfo("ttt").await.unwrap().unwrap();
256 let parsed = NarInfo::parse(&narinfo_text).unwrap();
257
258 let valid = crate::signing::verify_narinfo_signature(
260 &parsed,
261 &parsed.signatures[0],
262 &pk_str,
263 )
264 .unwrap();
265 assert!(valid);
266 }
267
268 #[test]
269 fn sha256_hex_produces_correct_output() {
270 let hash = sha256_hex(b"");
272 assert_eq!(
273 hash,
274 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
275 );
276 }
277
278 #[test]
279 fn compress_xz_produces_valid_output() {
280 let data = b"hello world, this is test data for xz compression";
281 let compressed = compress_xz(data).unwrap();
282 use std::io::Read;
284 let mut decoder = xz2::read::XzDecoder::new(compressed.as_slice());
285 let mut decompressed = Vec::new();
286 decoder.read_to_end(&mut decompressed).unwrap();
287 assert_eq!(decompressed, data);
288 }
289}