use std::io::Read as _;
use anyhow::anyhow;
use serde_json::{from_value, Value};
use super::{Firmware, Result};
#[derive(Clone, Debug)]
pub struct AssetSpec {
pub name: String,
pub url: String,
pub len: usize,
}
impl TryFrom<Value> for AssetSpec {
type Error = crate::Error;
fn try_from(value: Value) -> Result<Self> {
let name: String = from_value(value["name"].clone())?;
let url: String = from_value(value["browser_download_url"].clone())?;
let len: usize = from_value(value["size"].clone())?;
Ok(Self { name, url, len })
}
}
impl AssetSpec {
pub fn fetch_asset(&self) -> Result<Vec<u8>> {
let reader = ureq::get(&self.url)
.set("User-Agent", "solo2-cli")
.call()?
.into_reader();
let pb = indicatif::ProgressBar::new(self.len as _);
let mut buffer = Vec::new();
pb.wrap_read(reader).read_to_end(&mut buffer)?;
if self.len == buffer.len() {
Ok(buffer)
} else {
Err(anyhow!("Truncated download from {}", &self.url))
}
}
}
#[derive(Clone, Debug)]
pub struct Release {
pub tag: String,
pub assets: Vec<AssetSpec>,
}
impl Release {
const URL_LATEST: &'static str = "https://api.github.com/repos/solokeys/solo2/releases/latest";
const HASH_TEMPLATE: &'static str = "solo2-firmware-{}.sha2";
const SB2_TEMPLATE: &'static str = "solo2-firmware-{}.sb2";
pub fn fetch_spec() -> Result<Self> {
let response: Value = ureq::get(Self::URL_LATEST)
.set("User-Agent", "solo2-cli")
.call()?
.into_json()?;
let tag: String = from_value(response["tag_name"].clone())?;
let assets: Vec<Value> = from_value(response["assets"].clone())?;
let assets: Vec<AssetSpec> = assets
.into_iter()
.map(AssetSpec::try_from)
.filter_map(|x| x.ok())
.collect();
Ok(Self { tag, assets })
}
pub fn fetch_hash(&self) -> Result<String> {
let spec = self.assets.iter()
.find(|asset| asset.name == Self::HASH_TEMPLATE.replace("{}", &self.tag))
.ok_or_else(|| anyhow!("Unable to find hash digest in latest SoloKeys release. Please open ticket on solokeys.com/solo2 or contact hello@solokeys.com."))?;
let hash_data = &spec.fetch_asset()?;
let hash = std::str::from_utf8(hash_data)
.map_err(|_| anyhow!("Invalid hash digest in latest SoloKeys release. Please open ticket on solokeys.com/solo2 or contact hello@solokeys.com."))?;
let hash = hash.split_whitespace().next().unwrap().to_string();
Ok(hash)
}
pub fn fetch_firmware(&self) -> Result<Firmware> {
let spec = self.assets.iter()
.find(|asset| asset.name == Self::SB2_TEMPLATE.replace("{}", &self.tag))
.ok_or_else(|| anyhow!("Unable to find firmware SB2 file in latest SoloKeys release. Please open ticket on solokeys.com/solo2 or contact hello@solokeys.com."))?;
let firmware = Firmware::new(spec.fetch_asset()?)?;
firmware.verify_hexhash(&self.fetch_hash()?)?;
Ok(firmware)
}
}