1use std::collections::BTreeSet;
63use std::fs;
64use std::io::Read;
65use std::path::{Path, PathBuf};
66
67use anyhow::{Context, bail};
68use flate2::read::GzDecoder;
69use hex_literal::hex;
70use sha2::{Digest, Sha256};
71use walkdir::WalkDir;
72
73struct NpmPackage {
74 name: &'static str,
75 version: &'static str,
76 digest: [u8; 32],
77 css_file: Option<&'static str>,
78 js_prod_file: &'static str,
79 js_dev_file: &'static str,
80 extra_file: Option<(&'static str, &'static str)>,
81}
82
83const NPM_PACKAGES: &[NpmPackage] = &[
84 NpmPackage {
85 name: "@hpcc-js/wasm",
86 version: "0.3.14",
87 digest: hex!("b1628f561790925e58d33dcc5552aa2d1e8316a14b8436999a3c9c86df7c514a"),
88 css_file: None,
89 js_prod_file: "dist/index.min.js",
90 js_dev_file: "dist/index.js",
91 extra_file: Some((
92 "dist/graphvizlib.wasm",
93 "js/vendor/@hpcc-js/graphvizlib.wasm",
94 )),
95 },
96 NpmPackage {
97 name: "@babel/standalone",
98 version: "7.23.3",
99 digest: hex!("0f7b0deb19f1d0d77ff5dd05205be5063a930f2a6a56a6df92a6ed1e0cb73440"),
100 css_file: None,
101 js_prod_file: "babel.min.js",
102 js_dev_file: "babel.js",
103 extra_file: None,
104 },
105 NpmPackage {
106 name: "d3",
107 version: "5.16.0",
108 digest: hex!("85aa224591310c3cdd8a2ab8d3f8421bb7b0035926190389e790497c5b1d0f0b"),
109 css_file: None,
110 js_prod_file: "dist/d3.min.js",
111 js_dev_file: "dist/d3.js",
112 extra_file: None,
113 },
114 NpmPackage {
115 name: "d3-flame-graph",
116 version: "3.1.1",
117 digest: hex!("603120d8f1badfde6155816585d8e4c494f9783ae8fd40a3974928df707b1889"),
118 css_file: Some("dist/d3-flamegraph.css"),
119 js_prod_file: "dist/d3-flamegraph.min.js",
120 js_dev_file: "dist/d3-flamegraph.js",
121 extra_file: None,
122 },
123 NpmPackage {
124 name: "pako",
125 version: "1.0.11",
126 digest: hex!("1243d3fd9710c9b4e04d9528db02bfa55a4055bebc24743628fdddf59c83fa95"),
127 css_file: None,
128 js_prod_file: "dist/pako.min.js",
129 js_dev_file: "dist/pako.js",
130 extra_file: None,
131 },
132 NpmPackage {
133 name: "react",
134 version: "16.14.0",
135 digest: hex!("2fd361cfad2e0f8df36b67a0fdd43bd8064822b077fc7d70a84388918c663089"),
136 css_file: None,
137 js_prod_file: "umd/react.production.min.js",
138 js_dev_file: "umd/react.development.js",
139 extra_file: None,
140 },
141 NpmPackage {
142 name: "react-dom",
143 version: "16.14.0",
144 digest: hex!("27f6addacabaa5e5b9aa36ef443d4e79a947f3245c7d6c77f310f9c9fc944e25"),
145 css_file: None,
146 js_prod_file: "umd/react-dom.production.min.js",
147 js_dev_file: "umd/react-dom.development.js",
148 extra_file: None,
149 },
150];
151
152const STATIC: &str = "src/http/static";
153const CSS_VENDOR: &str = "src/http/static/css/vendor";
154const JS_PROD_VENDOR: &str = "src/http/static/js/vendor";
155const JS_DEV_VENDOR: &str = "src/http/static-dev/js/vendor";
156
157impl NpmPackage {
158 fn css_path(&self) -> PathBuf {
159 Path::new(CSS_VENDOR).join(format!("{}.css", self.name))
160 }
161
162 fn js_prod_path(&self) -> PathBuf {
163 Path::new(JS_PROD_VENDOR).join(format!("{}.js", self.name))
164 }
165
166 fn js_dev_path(&self) -> PathBuf {
167 Path::new(JS_DEV_VENDOR).join(format!("{}.js", self.name))
168 }
169
170 fn extra_path(&self) -> PathBuf {
171 let dst = self.extra_file.map(|(_src, dst)| dst);
172 Path::new(STATIC).join(dst.unwrap_or(""))
173 }
174
175 fn compute_digest(&self) -> Result<Vec<u8>, anyhow::Error> {
176 let css_data = if self.css_file.is_some() {
177 fs::read(self.css_path())?
178 } else {
179 vec![]
180 };
181 let js_prod_data = fs::read(self.js_prod_path())?;
182 let js_dev_data = fs::read(self.js_dev_path())?;
183 let extra_data = if self.extra_file.is_some() {
184 fs::read(self.extra_path())?
185 } else {
186 vec![]
187 };
188 Ok(Sha256::new()
189 .chain_update(Sha256::digest(css_data))
190 .chain_update(Sha256::digest(js_prod_data))
191 .chain_update(Sha256::digest(js_dev_data))
192 .chain_update(Sha256::digest(extra_data))
193 .finalize()
194 .as_slice()
195 .into())
196 }
197}
198
199pub fn ensure(out_dir: Option<PathBuf>) -> Result<(), anyhow::Error> {
200 println!("ensuring all npm packages are up-to-date...");
201 let attempts = 3;
202
203 let client = reqwest::blocking::Client::new();
204 for pkg in NPM_PACKAGES {
205 if pkg.compute_digest().ok().as_deref() == Some(&pkg.digest) {
206 println!("{} is up-to-date", pkg.name);
207 continue;
208 } else {
209 println!("{} needs updating...", pkg.name);
210 }
211
212 let mut success = false;
213 for attempt in 1..=attempts {
214 println!(
215 "downloading {} (attempt {}/{})...",
216 pkg.name, attempt, attempts
217 );
218
219 let url = format!(
220 "https://registry.npmjs.org/{}/-/{}-{}.tgz",
221 pkg.name,
222 pkg.name.split('/').next_back().unwrap(),
223 pkg.version,
224 );
225
226 let res = client
227 .get(&url)
228 .send()
229 .and_then(|res| res.error_for_status())
230 .with_context(|| format!("downloading {}", pkg.name))?;
231
232 let mut archive = tar::Archive::new(GzDecoder::new(res));
233 for entry in archive.entries()? {
234 let mut entry = entry?;
235 let path = entry.path()?.strip_prefix("package")?.to_owned();
236 if let Some(css_file) = &pkg.css_file {
237 if path == Path::new(css_file) {
238 unpack_entry(&mut entry, &pkg.css_path())?;
239 }
240 }
241 if path == Path::new(pkg.js_prod_file) {
242 unpack_entry(&mut entry, &pkg.js_prod_path())?;
243 }
244 if path == Path::new(pkg.js_dev_file) {
245 unpack_entry(&mut entry, &pkg.js_dev_path())?;
246 }
247 if let Some((extra_src, _extra_dst)) = &pkg.extra_file {
248 if path == Path::new(extra_src) {
249 unpack_entry(&mut entry, &pkg.extra_path())?;
250 }
251 }
252 }
253
254 let digest = pkg
255 .compute_digest()
256 .with_context(|| format!("computing digest for {}", pkg.name))?;
257 if digest == pkg.digest {
258 success = true;
259 println!("{} verified successfully.", pkg.name);
260 break;
261 } else {
262 eprintln!(
263 "digest mismatch for {} (attempt {}/{})\n expected: {}\n actual: {}",
264 pkg.name,
265 attempt,
266 attempts,
267 hex::encode(pkg.digest),
268 hex::encode(digest)
269 );
270 }
271 }
272
273 if !success {
274 bail!(
275 "npm package {} did not match expected digest after {} attempts",
276 pkg.name,
277 attempts
278 );
279 }
280 }
281
282 let mut known_paths = BTreeSet::new();
286 for pkg in NPM_PACKAGES {
287 if pkg.css_file.is_some() {
288 known_paths.insert(pkg.css_path());
289 }
290 known_paths.insert(pkg.js_prod_path());
291 known_paths.insert(pkg.js_dev_path());
292 if pkg.extra_file.is_some() {
293 known_paths.insert(pkg.extra_path());
294 }
295 }
296 for dir in &[CSS_VENDOR, JS_PROD_VENDOR, JS_DEV_VENDOR] {
297 for entry in WalkDir::new(dir) {
298 let entry = entry?;
299 if entry.file_type().is_file() {
300 if !known_paths.contains(entry.path()) {
301 println!("removing stray vendor file {}", entry.path().display());
302 fs::remove_file(entry.path())?;
303 } else if let Some(out_dir) = &out_dir {
304 let dst_path = out_dir.join(entry.path());
305 println!(
306 "copying path to OUT_DIR, src {}, dst {}",
307 entry.path().display(),
308 dst_path.display(),
309 );
310 if let Some(parent) = dst_path.parent() {
311 fs::create_dir_all(parent)?;
312 }
313 fs::copy(entry.path(), dst_path)?;
314 }
315 }
316 }
317 }
318
319 Ok(())
320}
321
322fn unpack_entry<T>(entry: &mut tar::Entry<T>, target: &Path) -> Result<(), anyhow::Error>
323where
324 T: Read,
325{
326 if let Some(parent) = target.parent() {
327 fs::create_dir_all(parent)
328 .with_context(|| format!("creating directory {}", parent.display()))?;
329 }
330 entry.unpack(target)?;
331 Ok(())
332}