1use crate::error::{Error, Result};
9use crate::extract::{extract_tar, extract_zip};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum DistKind {
14 Zip,
15 Tar,
16}
17use std::path::PathBuf;
18
19pub struct Store {
20 root: PathBuf,
21}
22
23impl Store {
24 pub fn at(root: PathBuf) -> Store {
25 Store { root }
26 }
27
28 pub fn default_location() -> Store {
29 Store::at(crate::platform::cache_dir().join("store"))
30 }
31
32 pub fn entry_path(&self, name: &str, version: &str, dist_ref: Option<&str>) -> PathBuf {
35 let sane = |s: &str| -> String {
36 s.chars()
37 .map(|c| {
38 if c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_') {
39 c
40 } else {
41 '-'
42 }
43 })
44 .collect()
45 };
46 let short_ref = dist_ref.unwrap_or("noref");
47 let short_ref = &short_ref[..short_ref.len().min(12)];
48 self.root
49 .join(name) .join(format!("{}-{}", sane(version), sane(short_ref)))
51 }
52
53 pub fn ensure(
56 &self,
57 name: &str,
58 version: &str,
59 dist_ref: Option<&str>,
60 dist_bytes: &[u8],
61 kind: DistKind,
62 ) -> Result<PathBuf> {
63 let final_path = self.entry_path(name, version, dist_ref);
64 if final_path.is_dir() {
65 return Ok(final_path);
66 }
67 let parent = final_path.parent().unwrap_or(&self.root).to_path_buf();
68 std::fs::create_dir_all(&parent).map_err(Error::io(&parent))?;
69 let tmp = tempfile::Builder::new()
70 .prefix(".tmp-")
71 .tempdir_in(&parent)
72 .map_err(Error::io(&parent))?;
73 match kind {
74 DistKind::Zip => extract_zip(dist_bytes, tmp.path())?,
75 DistKind::Tar => extract_tar(dist_bytes, tmp.path())?,
76 }
77 let tmp_path = tmp.keep();
78 match std::fs::rename(&tmp_path, &final_path) {
79 Ok(()) => Ok(final_path),
80 Err(_) if final_path.is_dir() => {
81 let _ = std::fs::remove_dir_all(&tmp_path);
83 Ok(final_path)
84 }
85 Err(source) => {
86 let _ = std::fs::remove_dir_all(&tmp_path);
87 Err(Error::Io {
88 path: final_path,
89 source,
90 })
91 }
92 }
93 }
94
95 pub fn contains(&self, name: &str, version: &str, dist_ref: Option<&str>) -> bool {
96 self.entry_path(name, version, dist_ref).is_dir()
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use std::io::Write as _;
104 use zip::write::SimpleFileOptions;
105
106 fn sample_zip() -> Vec<u8> {
107 let mut w = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
108 w.add_directory("root-x", SimpleFileOptions::default())
109 .expect("dir");
110 w.start_file("root-x/composer.json", SimpleFileOptions::default())
111 .expect("f");
112 w.write_all(b"{}").expect("w");
113 w.finish().expect("finish").into_inner()
114 }
115
116 #[test]
117 fn ensure_is_idempotent_and_atomic() {
118 let dir = tempfile::tempdir().expect("tmp");
119 let store = Store::at(dir.path().join("store"));
120 let zip = sample_zip();
121 let p1 = store
122 .ensure(
123 "a/b",
124 "1.0.0",
125 Some("deadbeefcafe1234"),
126 &zip,
127 DistKind::Zip,
128 )
129 .expect("ensure");
130 assert!(p1.join("composer.json").is_file());
131 assert!(store.contains("a/b", "1.0.0", Some("deadbeefcafe1234")));
132 let p2 = store
134 .ensure(
135 "a/b",
136 "1.0.0",
137 Some("deadbeefcafe1234"),
138 b"garbage",
139 DistKind::Zip,
140 )
141 .expect("hit");
142 assert_eq!(p1, p2);
143 assert!(!store.contains("a/b", "1.0.0", Some("feedfacefeed5678")));
145 let p3 = store
147 .ensure("a/b", "../../evil", None, &zip, DistKind::Zip)
148 .expect("sane");
149 assert!(p3.starts_with(dir.path().join("store").join("a/b")));
150 }
151}