1use aube_manifest::BundledDependencies;
2use aube_registry::{Attestations, Dist, NpmUser, Packument, PeerDepMeta, VersionMetadata};
3use std::collections::BTreeMap;
4use std::io::{Cursor, Write};
5use std::path::{Path, PathBuf};
6use std::sync::OnceLock;
7use std::time::Duration;
8
9#[path = "primer_schema.rs"]
10mod primer_schema;
11
12pub(crate) use primer_schema::Seed;
13use primer_schema::{
14 PrimerBundledDependencies, PrimerDist, PrimerPackument, PrimerPeerDepMeta,
15 PrimerVersionMetadata,
16};
17
18const PRIMER_FORMAT: &str = "rkyv-v1";
19const PRUNE_AGE: Duration = Duration::from_secs(30 * 24 * 60 * 60);
20const AUTO_PRUNE_COOLDOWN: Duration = Duration::from_secs(24 * 60 * 60);
21const AUTO_PRUNE_DENOMINATOR: u8 = 100;
22
23include!(concat!(env!("OUT_DIR"), "/primer_index.rs"));
24
25#[derive(Default)]
26pub struct PruneStats {
27 pub files: u64,
28 pub bytes: u64,
29}
30
31impl Seed {
32 pub(crate) fn packument(&self) -> Packument {
33 self.packument.to_packument()
34 }
35}
36
37impl PrimerPackument {
38 fn to_packument(&self) -> Packument {
39 let mut time = BTreeMap::new();
40 let versions = self
41 .versions
42 .iter()
43 .map(|v| {
44 if let Some(published_at) = v.published_at.as_ref() {
45 time.insert(v.version.clone(), published_at.clone());
46 }
47 (
48 v.version.clone(),
49 v.metadata.to_version_metadata(&self.name, &v.version),
50 )
51 })
52 .collect();
53 Packument {
54 name: self.name.clone(),
55 modified: self.modified.clone(),
56 versions,
57 dist_tags: self.dist_tags.clone(),
58 time,
59 }
60 }
61}
62
63impl PrimerVersionMetadata {
64 fn to_version_metadata(&self, name: &str, version: &str) -> VersionMetadata {
65 VersionMetadata {
66 name: name.to_owned(),
67 version: version.to_owned(),
68 dependencies: self.dependencies.clone(),
69 dev_dependencies: BTreeMap::new(),
70 peer_dependencies: self.peer_dependencies.clone(),
71 peer_dependencies_meta: self
72 .peer_dependencies_meta
73 .iter()
74 .map(|(name, meta)| (name.clone(), meta.to_peer_dep_meta()))
75 .collect(),
76 optional_dependencies: self.optional_dependencies.clone(),
77 bundled_dependencies: self
78 .bundled_dependencies
79 .as_ref()
80 .map(PrimerBundledDependencies::to_bundled_dependencies),
81 dist: self.dist.as_ref().map(|d| d.to_dist(name, version)),
82 os: self.os.clone(),
83 cpu: self.cpu.clone(),
84 libc: self.libc.clone(),
85 engines: self.engines.clone(),
86 license: self.license.clone(),
87 funding_url: self.funding_url.clone(),
88 bin: self.bin.clone(),
89 has_install_script: self.has_install_script,
90 deprecated: self.deprecated.clone(),
91 approver: None,
92 npm_user: self.trusted_publisher.then(|| NpmUser {
93 trusted_publisher: Some(serde_json::json!({"id": "npm-primer"})),
94 }),
95 }
96 }
97}
98
99impl PrimerPeerDepMeta {
100 fn to_peer_dep_meta(&self) -> PeerDepMeta {
101 PeerDepMeta {
102 optional: self.optional,
103 }
104 }
105}
106
107impl PrimerBundledDependencies {
108 fn to_bundled_dependencies(&self) -> BundledDependencies {
109 match self {
110 Self::List(v) => BundledDependencies::List(v.clone()),
111 Self::All(v) => BundledDependencies::All(*v),
112 }
113 }
114}
115
116impl PrimerDist {
117 fn to_dist(&self, name: &str, version: &str) -> Dist {
118 Dist {
119 tarball: self
120 .tarball
121 .clone()
122 .unwrap_or_else(|| deterministic_tarball_url(name, version)),
123 integrity: self.integrity.clone(),
124 shasum: None,
125 unpacked_size: None,
126 attestations: self.provenance.then(|| Attestations {
127 provenance: Some(serde_json::json!({
128 "predicateType": "https://slsa.dev/provenance/v1"
129 })),
130 }),
131 }
132 }
133}
134
135fn deterministic_tarball_url(name: &str, version: &str) -> String {
142 let unscoped = name
143 .strip_prefix('@')
144 .and_then(|rest| rest.split('/').nth(1))
145 .unwrap_or(name);
146 format!("https://registry.npmjs.org/{name}/-/{unscoped}-{version}.tgz")
147}
148
149static GENERATED_AT: OnceLock<Option<String>> = OnceLock::new();
150static AUTO_PRUNED: OnceLock<()> = OnceLock::new();
151static POPULAR_NAMES: OnceLock<Option<String>> = OnceLock::new();
152
153pub(crate) fn get(name: &str) -> Option<Seed> {
154 let (_, offset, len) = PRIMER_INDEX
155 .binary_search_by(|(candidate, _, _)| candidate.cmp(&name))
156 .ok()
157 .and_then(|idx| PRIMER_INDEX.get(idx))?;
158 auto_prune_once();
159 let end = offset.checked_add(*len)?;
160 let compressed = PRIMER_BLOB.get(*offset..end)?;
161 let archived = zstd::stream::decode_all(Cursor::new(compressed)).ok()?;
162 rkyv::from_bytes::<Seed, rkyv::rancor::Error>(&archived).ok()
163}
164
165pub(crate) fn covers_cutoff(cutoff: &str) -> bool {
166 generated_at().is_some_and(|generated_at| generated_at.as_str() >= cutoff)
167}
168
169pub fn popular_package_names() -> &'static str {
175 POPULAR_NAMES
176 .get_or_init(|| {
177 let decoded = zstd::stream::decode_all(Cursor::new(POPULAR_NAMES_BLOB)).ok()?;
178 String::from_utf8(decoded).ok()
179 })
180 .as_deref()
181 .unwrap_or_default()
182}
183
184fn generated_at() -> Option<&'static String> {
185 GENERATED_AT
186 .get_or_init(|| {
187 let secs = option_env!("AUBE_PRIMER_GENERATED_AT")?.parse().ok()?;
188 Some(crate::types::format_iso8601_utc(secs))
189 })
190 .as_ref()
191}
192
193fn auto_prune_once() {
194 AUTO_PRUNED.get_or_init(|| {
195 if let Some(dir) = primer_cache_dir() {
196 auto_prune(&dir);
197 }
198 });
199}
200
201fn auto_prune(dir: &Path) {
202 if !random_byte().is_multiple_of(AUTO_PRUNE_DENOMINATOR) {
203 return;
204 }
205 if let Err(e) = prune_old(dir, PRUNE_AGE, false, Some(AUTO_PRUNE_COOLDOWN)) {
206 tracing::debug!("failed to prune old primer cache files: {e}");
207 }
208}
209
210pub fn prune_cache(dry_run: bool, age: Duration) -> std::io::Result<PruneStats> {
211 let Some(dir) = primer_cache_dir() else {
212 return Ok(PruneStats::default());
213 };
214 prune_old(&dir, age, dry_run, None)
215}
216
217fn prune_old(
218 dir: &Path,
219 age: Duration,
220 dry_run: bool,
221 sentinel_cooldown: Option<Duration>,
222) -> std::io::Result<PruneStats> {
223 let mut stats = PruneStats::default();
224 std::fs::create_dir_all(dir)?;
225 let sentinel = dir.join(".auto_prune");
226 if let Some(cooldown) = sentinel_cooldown
227 && let Ok(modified) = sentinel.metadata().and_then(|m| m.modified())
228 && modified.elapsed().unwrap_or_default() < cooldown
229 {
230 return Ok(stats);
231 }
232 if sentinel_cooldown.is_some() {
233 touch(&sentinel)?;
234 }
235 let entries = std::fs::read_dir(dir)?;
236 for entry in entries {
237 let entry = entry?;
238 let path = entry.path();
239 let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
240 continue;
241 };
242 if !is_primer_cache_file(name) {
243 continue;
244 }
245 let metadata = entry.metadata()?;
246 if metadata.modified()?.elapsed().unwrap_or_default() > age {
247 stats.files += 1;
248 stats.bytes += metadata.len();
249 if !dry_run {
250 std::fs::remove_file(&path)?;
251 }
252 }
253 }
254 Ok(stats)
255}
256
257fn touch(path: &Path) -> std::io::Result<()> {
258 if let Some(parent) = path.parent() {
259 std::fs::create_dir_all(parent)?;
260 }
261 std::fs::OpenOptions::new()
262 .create(true)
263 .write(true)
264 .truncate(true)
265 .open(path)?
266 .write_all(b"\n")
267}
268
269fn is_primer_cache_file(name: &str) -> bool {
270 name.starts_with(&format!("{PRIMER_FORMAT}-")) && name.ends_with(".rkyv")
271}
272
273fn random_byte() -> u8 {
274 let nanos = std::time::SystemTime::now()
275 .duration_since(std::time::UNIX_EPOCH)
276 .map(|d| d.as_nanos())
277 .unwrap_or_default();
278 (nanos as u8) ^ (std::process::id() as u8)
279}
280
281fn primer_cache_dir() -> Option<PathBuf> {
282 if let Some(base) = aube_util::env::config_env("CACHE_DIR") {
287 return Some(PathBuf::from(base).join("primer"));
288 }
289 cache_base_dir().map(|p| p.join(aube_util::embedder().cache_namespace).join("primer"))
293}
294
295#[cfg(unix)]
296fn cache_base_dir() -> Option<PathBuf> {
297 std::env::var_os("XDG_CACHE_HOME")
298 .map(PathBuf::from)
299 .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache")))
300}
301
302#[cfg(windows)]
303fn cache_base_dir() -> Option<PathBuf> {
304 std::env::var_os("LOCALAPPDATA").map(PathBuf::from)
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn bundled_primer_loads() {
313 let Some((name, _, _)) = PRIMER_INDEX.first() else {
314 return;
315 };
316 assert!(super::get(name).is_some());
317 }
318
319 #[test]
320 fn bundled_popular_names_load() {
321 let names = popular_package_names();
322 if !PRIMER_INDEX.is_empty() {
323 assert!(!names.is_empty());
324 }
325 assert!(names.lines().all(|name| !name.is_empty()));
326 }
327
328 #[test]
329 fn bundled_primer_synthesizes_tarball_urls() {
330 let Some((name, _, _)) = PRIMER_INDEX.first() else {
337 return;
338 };
339 let packument = super::get(name).expect("primer hit").packument();
340 let (version, meta) = packument
341 .versions
342 .iter()
343 .find(|(_, v)| v.dist.is_some())
344 .expect("packument has at least one version with dist metadata");
345 let dist = meta.dist.as_ref().unwrap();
346 assert!(
347 dist.tarball.starts_with("https://"),
348 "tarball: {}",
349 dist.tarball
350 );
351 assert!(dist.tarball.ends_with(".tgz"), "tarball: {}", dist.tarball);
352 assert!(
353 dist.tarball.contains(*name),
354 "tarball {} missing package name {name}",
355 dist.tarball,
356 );
357 assert!(
358 dist.tarball.contains(version),
359 "tarball {} missing version {version}",
360 dist.tarball,
361 );
362 }
363
364 #[test]
365 fn deterministic_tarball_url_handles_scoped_names() {
366 assert_eq!(
367 deterministic_tarball_url("react", "18.2.0"),
368 "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
369 );
370 assert_eq!(
371 deterministic_tarball_url("@types/node", "20.10.0"),
372 "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz"
373 );
374 }
375
376 #[test]
377 fn primer_cache_file_match_is_narrow() {
378 assert!(is_primer_cache_file("rkyv-v1-abc.rkyv"));
379 assert!(!is_primer_cache_file(".auto_prune"));
380 assert!(!is_primer_cache_file("rkyv-v1-abc.tmp"));
381 assert!(!is_primer_cache_file("other-v1-abc.rkyv"));
382 }
383
384 #[test]
385 fn prune_removes_old_extracted_primer_files() {
386 let temp = tempfile::tempdir().unwrap();
387 let dir = temp.path();
388 std::fs::write(dir.join("rkyv-v1-old-0-old.rkyv"), "{}").unwrap();
389 std::fs::write(dir.join("packument.json"), "{}").unwrap();
390 let stats = prune_old(dir, Duration::from_secs(0), false, None).unwrap();
391 assert_eq!(stats.files, 1);
392 assert!(!dir.join("rkyv-v1-old-0-old.rkyv").exists());
393 assert!(dir.join("packument.json").exists());
394 }
395
396 #[test]
397 fn prune_sentinel_uses_own_cooldown() {
398 let temp = tempfile::tempdir().unwrap();
399 let dir = temp.path();
400 let primer_file = dir.join("rkyv-v1-old-0-old.rkyv");
401 std::fs::write(&primer_file, "{}").unwrap();
402 touch(&dir.join(".auto_prune")).unwrap();
403
404 let stats = prune_old(
405 dir,
406 Duration::from_secs(0),
407 false,
408 Some(Duration::from_secs(60)),
409 )
410 .unwrap();
411
412 assert_eq!(stats.files, 0);
413 assert!(primer_file.exists());
414 }
415}