Generalizes the digest verification logic from `rust-analyzer` and `clangd` into a reusable helper function in `http_client::github_download`. This removes ~100 lines of duplicated code across the two language adapters and makes it easier for other language servers to adopt digest verification in the future. Closes #35201 Release Notes: - N/A
249 lines
8.0 KiB
Rust
249 lines
8.0 KiB
Rust
use std::{future::Future, path::Path, pin::Pin, task::Poll};
|
|
|
|
use anyhow::{Context, Result};
|
|
use async_compression::futures::bufread::GzipDecoder;
|
|
use futures::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, io::BufReader};
|
|
use sha2::{Digest, Sha256};
|
|
|
|
use crate::{HttpClient, github::AssetKind};
|
|
|
|
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
|
pub struct GithubBinaryMetadata {
|
|
pub metadata_version: u64,
|
|
pub digest: Option<String>,
|
|
}
|
|
|
|
impl GithubBinaryMetadata {
|
|
pub async fn read_from_file(metadata_path: &Path) -> Result<GithubBinaryMetadata> {
|
|
let metadata_content = async_fs::read_to_string(metadata_path)
|
|
.await
|
|
.with_context(|| format!("reading metadata file at {metadata_path:?}"))?;
|
|
serde_json::from_str(&metadata_content)
|
|
.with_context(|| format!("parsing metadata file at {metadata_path:?}"))
|
|
}
|
|
|
|
pub async fn write_to_file(&self, metadata_path: &Path) -> Result<()> {
|
|
let metadata_content = serde_json::to_string(self)
|
|
.with_context(|| format!("serializing metadata for {metadata_path:?}"))?;
|
|
async_fs::write(metadata_path, metadata_content.as_bytes())
|
|
.await
|
|
.with_context(|| format!("writing metadata file at {metadata_path:?}"))?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub async fn download_server_binary(
|
|
http_client: &dyn HttpClient,
|
|
url: &str,
|
|
digest: Option<&str>,
|
|
destination_path: &Path,
|
|
asset_kind: AssetKind,
|
|
) -> Result<(), anyhow::Error> {
|
|
log::info!("downloading github artifact from {url}");
|
|
let mut response = http_client
|
|
.get(url, Default::default(), true)
|
|
.await
|
|
.with_context(|| format!("downloading release from {url}"))?;
|
|
let body = response.body_mut();
|
|
match digest {
|
|
Some(expected_sha_256) => {
|
|
let temp_asset_file = tempfile::NamedTempFile::new()
|
|
.with_context(|| format!("creating a temporary file for {url}"))?;
|
|
let (temp_asset_file, _temp_guard) = temp_asset_file.into_parts();
|
|
let mut writer = HashingWriter {
|
|
writer: async_fs::File::from(temp_asset_file),
|
|
hasher: Sha256::new(),
|
|
};
|
|
futures::io::copy(&mut BufReader::new(body), &mut writer)
|
|
.await
|
|
.with_context(|| {
|
|
format!("saving archive contents into the temporary file for {url}",)
|
|
})?;
|
|
let asset_sha_256 = format!("{:x}", writer.hasher.finalize());
|
|
|
|
anyhow::ensure!(
|
|
asset_sha_256 == expected_sha_256,
|
|
"{url} asset got SHA-256 mismatch. Expected: {expected_sha_256}, Got: {asset_sha_256}",
|
|
);
|
|
writer
|
|
.writer
|
|
.seek(std::io::SeekFrom::Start(0))
|
|
.await
|
|
.with_context(|| format!("seeking temporary file {destination_path:?}",))?;
|
|
stream_file_archive(&mut writer.writer, url, destination_path, asset_kind)
|
|
.await
|
|
.with_context(|| {
|
|
format!("extracting downloaded asset for {url} into {destination_path:?}",)
|
|
})?;
|
|
}
|
|
None => stream_response_archive(body, url, destination_path, asset_kind)
|
|
.await
|
|
.with_context(|| {
|
|
format!("extracting response for asset {url} into {destination_path:?}",)
|
|
})?,
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn fetch_github_binary_with_digest_check<ValidityCheck, ValidityCheckFuture>(
|
|
binary_path: &Path,
|
|
metadata_path: &Path,
|
|
expected_digest: Option<String>,
|
|
url: &str,
|
|
asset_kind: AssetKind,
|
|
download_destination: &Path,
|
|
http_client: &dyn HttpClient,
|
|
validity_check: ValidityCheck,
|
|
) -> Result<()>
|
|
where
|
|
ValidityCheck: FnOnce() -> ValidityCheckFuture,
|
|
ValidityCheckFuture: Future<Output = Result<()>>,
|
|
{
|
|
let metadata = GithubBinaryMetadata::read_from_file(metadata_path)
|
|
.await
|
|
.ok();
|
|
|
|
if let Some(metadata) = metadata {
|
|
let validity_check_result = validity_check().await;
|
|
|
|
if let (Some(actual_digest), Some(expected_digest_ref)) =
|
|
(&metadata.digest, &expected_digest)
|
|
{
|
|
if actual_digest == expected_digest_ref {
|
|
if validity_check_result.is_ok() {
|
|
return Ok(());
|
|
}
|
|
} else {
|
|
log::info!(
|
|
"SHA-256 mismatch for {binary_path:?} asset, downloading new asset. Expected: {expected_digest_ref}, Got: {actual_digest}"
|
|
);
|
|
}
|
|
} else if validity_check_result.is_ok() {
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
download_server_binary(
|
|
http_client,
|
|
url,
|
|
expected_digest.as_deref(),
|
|
download_destination,
|
|
asset_kind,
|
|
)
|
|
.await?;
|
|
|
|
GithubBinaryMetadata::write_to_file(
|
|
&GithubBinaryMetadata {
|
|
metadata_version: 1,
|
|
digest: expected_digest,
|
|
},
|
|
metadata_path,
|
|
)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn stream_response_archive(
|
|
response: impl AsyncRead + Unpin,
|
|
url: &str,
|
|
destination_path: &Path,
|
|
asset_kind: AssetKind,
|
|
) -> Result<()> {
|
|
match asset_kind {
|
|
AssetKind::TarGz => extract_tar_gz(destination_path, url, response).await?,
|
|
AssetKind::Gz => extract_gz(destination_path, url, response).await?,
|
|
AssetKind::Zip => {
|
|
util::archive::extract_zip(destination_path, response).await?;
|
|
}
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
async fn stream_file_archive(
|
|
file_archive: impl AsyncRead + AsyncSeek + Unpin,
|
|
url: &str,
|
|
destination_path: &Path,
|
|
asset_kind: AssetKind,
|
|
) -> Result<()> {
|
|
match asset_kind {
|
|
AssetKind::TarGz => extract_tar_gz(destination_path, url, file_archive).await?,
|
|
AssetKind::Gz => extract_gz(destination_path, url, file_archive).await?,
|
|
#[cfg(not(windows))]
|
|
AssetKind::Zip => {
|
|
util::archive::extract_seekable_zip(destination_path, file_archive).await?;
|
|
}
|
|
#[cfg(windows)]
|
|
AssetKind::Zip => {
|
|
util::archive::extract_zip(destination_path, file_archive).await?;
|
|
}
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
async fn extract_tar_gz(
|
|
destination_path: &Path,
|
|
url: &str,
|
|
from: impl AsyncRead + Unpin,
|
|
) -> Result<(), anyhow::Error> {
|
|
let decompressed_bytes = GzipDecoder::new(BufReader::new(from));
|
|
let archive = async_tar::Archive::new(decompressed_bytes);
|
|
archive
|
|
.unpack(&destination_path)
|
|
.await
|
|
.with_context(|| format!("extracting {url} to {destination_path:?}"))?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn extract_gz(
|
|
destination_path: &Path,
|
|
url: &str,
|
|
from: impl AsyncRead + Unpin,
|
|
) -> Result<(), anyhow::Error> {
|
|
let mut decompressed_bytes = GzipDecoder::new(BufReader::new(from));
|
|
let mut file = async_fs::File::create(&destination_path)
|
|
.await
|
|
.with_context(|| {
|
|
format!("creating a file {destination_path:?} for a download from {url}")
|
|
})?;
|
|
futures::io::copy(&mut decompressed_bytes, &mut file)
|
|
.await
|
|
.with_context(|| format!("extracting {url} to {destination_path:?}"))?;
|
|
Ok(())
|
|
}
|
|
|
|
struct HashingWriter<W: AsyncWrite + Unpin> {
|
|
writer: W,
|
|
hasher: Sha256,
|
|
}
|
|
|
|
impl<W: AsyncWrite + Unpin> AsyncWrite for HashingWriter<W> {
|
|
fn poll_write(
|
|
mut self: Pin<&mut Self>,
|
|
cx: &mut std::task::Context<'_>,
|
|
buf: &[u8],
|
|
) -> Poll<std::result::Result<usize, std::io::Error>> {
|
|
match Pin::new(&mut self.writer).poll_write(cx, buf) {
|
|
Poll::Ready(Ok(n)) => {
|
|
self.hasher.update(&buf[..n]);
|
|
Poll::Ready(Ok(n))
|
|
}
|
|
other => other,
|
|
}
|
|
}
|
|
|
|
fn poll_flush(
|
|
mut self: Pin<&mut Self>,
|
|
cx: &mut std::task::Context<'_>,
|
|
) -> Poll<Result<(), std::io::Error>> {
|
|
Pin::new(&mut self.writer).poll_flush(cx)
|
|
}
|
|
|
|
fn poll_close(
|
|
mut self: Pin<&mut Self>,
|
|
cx: &mut std::task::Context<'_>,
|
|
) -> Poll<std::result::Result<(), std::io::Error>> {
|
|
Pin::new(&mut self.writer).poll_close(cx)
|
|
}
|
|
}
|