eska_tool_runtime/
installation.rs1use crate::{Error, contract};
4use serde_json::Value;
5use std::{fs, io::Read, path::Path};
6
7pub fn verify(binary: &Path, tool: &str) -> Result<(), Error> {
12 let name = format!("{tool}{}", std::env::consts::EXE_SUFFIX);
13 let Some(bin) = binary
14 .parent()
15 .filter(|path| path.file_name().is_some_and(|name| name == "bin"))
16 else {
17 return Ok(());
18 };
19 if binary.file_name() != Some(std::ffi::OsStr::new(&name)) {
20 return Ok(());
21 }
22 let Some(root) = bin.parent() else {
23 return Ok(());
24 };
25 let directory = root.join("components");
26 let receipt = directory.join(format!("{tool}.json"));
27 let invalid = || Error::Incompatible {
28 tool: tool.to_owned(),
29 receipt: receipt.clone(),
30 };
31 match fs::symlink_metadata(&receipt) {
32 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
33 Err(_) => return Err(invalid()),
34 Ok(info) if !info.is_file() || info.len() > 4 * 1024 * 1024 => return Err(invalid()),
35 Ok(_) => {}
36 }
37 if !fs::symlink_metadata(directory).is_ok_and(|info| info.is_dir()) {
38 return Err(invalid());
39 }
40 let mut data = Vec::new();
41 fs::File::open(&receipt)
42 .map_err(|_| invalid())?
43 .take(4 * 1024 * 1024 + 1)
44 .read_to_end(&mut data)
45 .map_err(|_| invalid())?;
46 if data.len() > 4 * 1024 * 1024 {
47 return Err(invalid());
48 }
49 let value: Value = serde_json::from_slice(&data).map_err(|_| invalid())?;
50 let package = &value["package"];
51 let metadata = &package["contract"];
52 let valid = value.as_object().is_some_and(|object| object.len() == 4)
53 && value["schema_version"] == 1
54 && value["method"] == "component-archive"
55 && sha256(&value["archive_sha256"])
56 && package.as_object().is_some_and(|object| object.len() == 6)
57 && package["schema_version"] == 1
58 && package["tool"] == tool
59 && package["version"] == metadata["version"]
60 && package["target"] == contract::TARGET
61 && contract::compatible(metadata, tool)
62 && package["files"].as_array().is_some_and(|files| {
63 let matches: Vec<_> = files
64 .iter()
65 .filter(|item| item["path"] == format!("bin/{name}"))
66 .collect();
67 (2..=8192).contains(&files.len())
68 && matches.len() == 1
69 && matches[0]["executable"] == true
70 && sha256(&matches[0]["sha256"])
71 && fs::metadata(binary)
72 .is_ok_and(|info| matches[0]["size"].as_u64() == Some(info.len()))
73 });
74 if valid { Ok(()) } else { Err(invalid()) }
75}
76
77fn sha256(value: &Value) -> bool {
79 value.as_str().is_some_and(|text| {
80 text.len() == 64
81 && text
82 .bytes()
83 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
84 })
85}