use std::error::Error; use std::fmt::{Debug, Display, Formatter}; use std::io::Write; use std::path::{PathBuf}; use log::debug; use lzma_rs::decompress; use reqwest::{Client, RequestBuilder}; use sha1_smol::{Digest, Sha1}; use tokio::io::AsyncWriteExt; use tokio::fs::File; use crate::launcher::download::Download; use crate::launcher::jre::manifest::JavaRuntimeFile; use crate::util; use crate::util::IntegrityError; use crate::version::DownloadInfo; pub enum LzmaDownloadError { NotAFile, MissingURL } pub struct LzmaDownloadJob { url: String, path: PathBuf, inflate: bool, executable: bool, raw_size: Option, raw_sha1: Option, raw_sha1_st: Sha1, raw_tally: usize, stream: Option>>, out_file: Option } impl LzmaDownloadJob { fn new_inflate(raw: &DownloadInfo, lzma: &DownloadInfo, exe: bool, path: PathBuf) -> Result { Ok(LzmaDownloadJob { url: lzma.url.as_ref().map_or_else(|| Err(LzmaDownloadError::MissingURL), |u| Ok(u.to_owned()))?, path, inflate: true, executable: exe, raw_size: raw.size, raw_sha1: raw.sha1, raw_sha1_st: Sha1::new(), raw_tally: 0, stream: Some(decompress::Stream::new(Vec::new())), out_file: None }) } fn new_raw(raw: &DownloadInfo, exe: bool, path: PathBuf) -> Result { Ok(LzmaDownloadJob { url: raw.url.as_ref().map_or_else(|| Err(LzmaDownloadError::MissingURL), |u| Ok(u.to_owned()))?, path, inflate: false, executable: exe, raw_size: raw.size, raw_sha1: raw.sha1, raw_sha1_st: Sha1::new(), raw_tally: 0, stream: None, out_file: None }) } } impl TryFrom<(&JavaRuntimeFile, PathBuf)> for LzmaDownloadJob { type Error = LzmaDownloadError; fn try_from((file, path): (&JavaRuntimeFile, PathBuf)) -> Result { if !file.is_file() { return Err(LzmaDownloadError::NotAFile); } let JavaRuntimeFile::File { executable, downloads } = file else { unreachable!("we just made sure this was a file"); }; match downloads.lzma.as_ref() { Some(lzma) => LzmaDownloadJob::new_inflate(&downloads.raw, lzma, *executable, path), None => LzmaDownloadJob::new_raw(&downloads.raw, *executable, path) } } } impl Debug for LzmaDownloadJob { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("LzmaDownloadJob") .field("url", &self.url) .field("path", &self.path) .field("inflate", &self.inflate) .finish() } } impl Display for LzmaDownloadJob { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if self.inflate { write!(f, "download and inflate {} to {}", &self.url, self.path.display()) } else { write!(f, "download {} to {}", &self.url, self.path.display()) } } } impl Download for LzmaDownloadJob { async fn prepare(&mut self, client: &Client) -> Result, Box> { if !util::should_download(&self.path, self.raw_size, self.raw_sha1).await? { return Ok(None) } let mut options = File::options(); #[cfg(unix)] { options.mode(match self.executable { true => 0o775, _ => 0o664 }); } let file = options.create(true).write(true).truncate(true).open(&self.path).await?; self.out_file = Some(file); Ok(Some(client.get(&self.url))) } async fn handle_chunk(&mut self, chunk: &[u8]) -> Result<(), Box> { let out_file = self.out_file.as_mut().expect("output file gone"); if let Some(ref mut stream) = self.stream { stream.write_all(chunk)?; let buf = stream.get_output_mut().expect("stream output missing before finish()"); out_file.write_all(buf.as_slice()).await?; self.raw_sha1_st.update(buf.as_slice()); self.raw_tally += buf.len(); buf.truncate(0); } else { out_file.write_all(chunk).await?; self.raw_sha1_st.update(chunk); self.raw_tally += chunk.len(); } Ok(()) } async fn finish(&mut self) -> Result<(), Box> { let mut out_file = self.out_file.take().expect("output file gone"); if let Some(stream) = self.stream.take() { let buf = stream.finish()?; out_file.write_all(buf.as_slice()).await?; self.raw_sha1_st.update(buf.as_slice()); self.raw_tally += buf.len(); } let inf_digest = self.raw_sha1_st.digest(); if let Some(sha1) = self.raw_sha1 { if inf_digest != sha1 { debug!("Could not download {}: sha1 mismatch (exp {}, got {}).", self.path.display(), sha1, inf_digest); return Err(IntegrityError::Sha1Mismatch { expect: sha1, actual: inf_digest }.into()); } } if let Some(size) = self.raw_size { if self.raw_tally != size { debug!("Could not download {}: size mismatch (exp {}, got {}).", self.path.display(), size, self.raw_tally); return Err(IntegrityError::SizeMismatch { expect: size, actual: self.raw_tally }.into()); } } Ok(()) } }