#![allow(clippy::style)]
#![allow(clippy::complexity)]
#![allow(clippy::large_enum_variant)]
#![allow(clippy::mutable_key_type)]
#![allow(clippy::stable_sort_primitive)]
#![allow(clippy::map_entry)]
#![allow(clippy::box_default)]
#![warn(clippy::bool_comparison)]
#![warn(clippy::clone_on_ref_ptr)]
#![warn(clippy::no_effect)]
#![warn(clippy::unnecessary_unwrap)]
#![warn(clippy::dbg_macro)]
#![warn(clippy::todo)]
#![warn(clippy::wildcard_dependencies)]
#![warn(clippy::zero_prefixed_literal)]
#![warn(clippy::borrowed_box)]
#![warn(clippy::deref_addrof)]
#![warn(clippy::double_must_use)]
#![warn(clippy::double_parens)]
#![warn(clippy::extra_unused_lifetimes)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::needless_question_mark)]
#![warn(clippy::needless_return)]
#![warn(clippy::redundant_pattern)]
#![warn(clippy::redundant_slicing)]
#![warn(clippy::redundant_static_lifetimes)]
#![warn(clippy::single_component_path_imports)]
#![warn(clippy::unnecessary_cast)]
#![warn(clippy::useless_asref)]
#![warn(clippy::useless_conversion)]
#![warn(clippy::builtin_type_shadow)]
#![warn(clippy::duplicate_underscore_argument)]
#![warn(clippy::double_neg)]
#![warn(clippy::unnecessary_mut_passed)]
#![warn(clippy::wildcard_in_or_patterns)]
#![warn(clippy::crosspointer_transmute)]
#![warn(clippy::excessive_precision)]
#![warn(clippy::overflow_check_conditional)]
#![warn(clippy::as_conversions)]
#![warn(clippy::match_overlapping_arm)]
#![warn(clippy::zero_divided_by_zero)]
#![warn(clippy::must_use_unit)]
#![warn(clippy::suspicious_assignment_formatting)]
#![warn(clippy::suspicious_else_formatting)]
#![warn(clippy::suspicious_unary_op_formatting)]
#![warn(clippy::mut_mutex_lock)]
#![warn(clippy::print_literal)]
#![warn(clippy::same_item_push)]
#![warn(clippy::useless_format)]
#![warn(clippy::write_literal)]
#![warn(clippy::redundant_closure)]
#![warn(clippy::redundant_closure_call)]
#![warn(clippy::unnecessary_lazy_evaluations)]
#![warn(clippy::partialeq_ne_impl)]
#![warn(clippy::redundant_field_names)]
#![warn(clippy::transmutes_expressible_as_ptr_casts)]
#![warn(clippy::unused_async)]
#![warn(clippy::disallowed_methods)]
#![warn(clippy::disallowed_macros)]
#![warn(clippy::disallowed_types)]
#![warn(clippy::from_over_into)]
use std::collections::BTreeSet;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context};
use flate2::read::GzDecoder;
use hex_literal::hex;
use sha2::{Digest, Sha256};
use walkdir::WalkDir;
struct NpmPackage {
name: &'static str,
version: &'static str,
digest: [u8; 32],
css_file: Option<&'static str>,
js_prod_file: &'static str,
js_dev_file: &'static str,
extra_file: Option<(&'static str, &'static str)>,
}
const NPM_PACKAGES: &[NpmPackage] = &[
NpmPackage {
name: "@hpcc-js/wasm",
version: "0.3.14",
digest: hex!("b1628f561790925e58d33dcc5552aa2d1e8316a14b8436999a3c9c86df7c514a"),
css_file: None,
js_prod_file: "dist/index.min.js",
js_dev_file: "dist/index.js",
extra_file: Some((
"dist/graphvizlib.wasm",
"js/vendor/@hpcc-js/graphvizlib.wasm",
)),
},
NpmPackage {
name: "babel-standalone",
version: "6.26.0",
digest: hex!("2a6dc2f1acc2893e53cb53192eee2dfc6a09d86ef7620dcb6323c4d624f99d92"),
css_file: None,
js_prod_file: "babel.min.js",
js_dev_file: "babel.js",
extra_file: None,
},
NpmPackage {
name: "d3",
version: "5.16.0",
digest: hex!("85aa224591310c3cdd8a2ab8d3f8421bb7b0035926190389e790497c5b1d0f0b"),
css_file: None,
js_prod_file: "dist/d3.min.js",
js_dev_file: "dist/d3.js",
extra_file: None,
},
NpmPackage {
name: "d3-flame-graph",
version: "3.1.1",
digest: hex!("603120d8f1badfde6155816585d8e4c494f9783ae8fd40a3974928df707b1889"),
css_file: Some("dist/d3-flamegraph.css"),
js_prod_file: "dist/d3-flamegraph.min.js",
js_dev_file: "dist/d3-flamegraph.js",
extra_file: None,
},
NpmPackage {
name: "pako",
version: "1.0.11",
digest: hex!("1243d3fd9710c9b4e04d9528db02bfa55a4055bebc24743628fdddf59c83fa95"),
css_file: None,
js_prod_file: "dist/pako.min.js",
js_dev_file: "dist/pako.js",
extra_file: None,
},
NpmPackage {
name: "react",
version: "16.14.0",
digest: hex!("2fd361cfad2e0f8df36b67a0fdd43bd8064822b077fc7d70a84388918c663089"),
css_file: None,
js_prod_file: "umd/react.production.min.js",
js_dev_file: "umd/react.development.js",
extra_file: None,
},
NpmPackage {
name: "react-dom",
version: "16.14.0",
digest: hex!("27f6addacabaa5e5b9aa36ef443d4e79a947f3245c7d6c77f310f9c9fc944e25"),
css_file: None,
js_prod_file: "umd/react-dom.production.min.js",
js_dev_file: "umd/react-dom.development.js",
extra_file: None,
},
];
const STATIC: &str = "src/http/static";
const CSS_VENDOR: &str = "src/http/static/css/vendor";
const JS_PROD_VENDOR: &str = "src/http/static/js/vendor";
const JS_DEV_VENDOR: &str = "src/http/static-dev/js/vendor";
impl NpmPackage {
fn css_path(&self) -> PathBuf {
Path::new(CSS_VENDOR).join(format!("{}.css", self.name))
}
fn js_prod_path(&self) -> PathBuf {
Path::new(JS_PROD_VENDOR).join(format!("{}.js", self.name))
}
fn js_dev_path(&self) -> PathBuf {
Path::new(JS_DEV_VENDOR).join(format!("{}.js", self.name))
}
fn extra_path(&self) -> PathBuf {
let dst = self.extra_file.map(|(_src, dst)| dst);
Path::new(STATIC).join(dst.unwrap_or(""))
}
fn compute_digest(&self) -> Result<Vec<u8>, anyhow::Error> {
let css_data = if self.css_file.is_some() {
fs::read(self.css_path())?
} else {
vec![]
};
let js_prod_data = fs::read(self.js_prod_path())?;
let js_dev_data = fs::read(self.js_dev_path())?;
let extra_data = if self.extra_file.is_some() {
fs::read(self.extra_path())?
} else {
vec![]
};
Ok(Sha256::new()
.chain_update(Sha256::digest(css_data))
.chain_update(Sha256::digest(js_prod_data))
.chain_update(Sha256::digest(js_dev_data))
.chain_update(Sha256::digest(extra_data))
.finalize()
.as_slice()
.into())
}
}
pub fn ensure() -> Result<(), anyhow::Error> {
println!("ensuring all npm packages are up-to-date...");
let client = reqwest::blocking::Client::new();
for pkg in NPM_PACKAGES {
if pkg.compute_digest().ok().as_deref() == Some(&pkg.digest) {
println!("{} is up-to-date", pkg.name);
continue;
} else {
println!("{} needs updating...", pkg.name);
}
let url = format!(
"https://registry.npmjs.org/{}/-/{}-{}.tgz",
pkg.name,
pkg.name.split('/').last().unwrap(),
pkg.version,
);
let res = client
.get(url)
.send()
.and_then(|res| res.error_for_status())
.with_context(|| format!("downloading {}", pkg.name))?;
let mut archive = tar::Archive::new(GzDecoder::new(res));
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?.strip_prefix("package")?.to_owned();
if let Some(css_file) = &pkg.css_file {
if path == Path::new(css_file) {
unpack_entry(&mut entry, &pkg.css_path())?;
}
}
if path == Path::new(pkg.js_prod_file) {
unpack_entry(&mut entry, &pkg.js_prod_path())?;
}
if path == Path::new(pkg.js_dev_file) {
unpack_entry(&mut entry, &pkg.js_dev_path())?;
}
if let Some((extra_src, _extra_dst)) = &pkg.extra_file {
if path == Path::new(extra_src) {
unpack_entry(&mut entry, &pkg.extra_path())?;
}
}
}
let digest = pkg
.compute_digest()
.with_context(|| format!("computing digest for {}", pkg.name))?;
if digest != pkg.digest {
bail!(
"npm package {} did not match expected digest
expected: {}
actual: {}",
pkg.name,
hex::encode(pkg.digest),
hex::encode(digest),
);
}
}
let mut known_paths = BTreeSet::new();
for pkg in NPM_PACKAGES {
if pkg.css_file.is_some() {
known_paths.insert(pkg.css_path());
}
known_paths.insert(pkg.js_prod_path());
known_paths.insert(pkg.js_dev_path());
if pkg.extra_file.is_some() {
known_paths.insert(pkg.extra_path());
}
}
for dir in &[CSS_VENDOR, JS_PROD_VENDOR, JS_DEV_VENDOR] {
for entry in WalkDir::new(dir) {
let entry = entry?;
if entry.file_type().is_file() && !known_paths.contains(entry.path()) {
println!("removing stray vendor file {}", entry.path().display());
fs::remove_file(entry.path())?;
}
}
}
Ok(())
}
fn unpack_entry<T>(entry: &mut tar::Entry<T>, target: &Path) -> Result<(), anyhow::Error>
where
T: Read,
{
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("creating directory {}", parent.display()))?;
}
entry.unpack(target)?;
Ok(())
}