Files
Starlight_Lancher/packages/app-lib/src/launcher/local_artifact.rs

143 lines
4.5 KiB
Rust

//! Single-pass verification and copy of artifacts from an external runtime.
use crate::util::fetch::{self, IoSemaphore};
use crate::util::io;
use std::path::Path;
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
pub(crate) async fn candidate_is_usable(
local: Option<&super::download::LocalRuntimeSource>,
relative_path: &Path,
expected_size: Option<u64>,
) -> crate::Result<bool> {
let Some(local) = local else {
return Ok(false);
};
let candidate = local.root.join(relative_path);
let metadata = match tokio::fs::metadata(candidate).await {
Ok(metadata) => metadata,
Err(_) => return Ok(false),
};
Ok(metadata.is_file()
&& expected_size.is_none_or(|size| metadata.len() == size))
}
/// Copy a local artifact through a temporary sibling while calculating its
/// SHA-1 from the exact bytes written. Returns `false` for a missing, wrong-
/// sized, or hash-mismatched source so the caller can use the network path.
pub(crate) async fn copy_verified(
source: &Path,
destination: &Path,
expected_sha1: Option<&str>,
expected_size: Option<u64>,
semaphore: &IoSemaphore,
) -> crate::Result<bool> {
let _permit = semaphore.0.acquire().await?;
let metadata = match tokio::fs::metadata(source).await {
Ok(metadata) if metadata.is_file() => metadata,
Ok(_) | Err(_) => return Ok(false),
};
if expected_size.is_some_and(|size| metadata.len() != size) {
return Ok(false);
}
let Some(expected_sha1) = expected_sha1 else {
return Ok(false);
};
if let Some(parent) = destination.parent() {
io::create_dir_all(parent).await?;
}
let part_path = fetch::suffixed_path(destination, ".part");
let mut input = File::open(source).await?;
let mut output = File::create(&part_path).await?;
let mut hasher = sha1_smol::Sha1::new();
let mut copied = 0_u64;
let mut buffer = vec![0_u8; 256 * 1024];
let copy_result = async {
loop {
let read = input.read(&mut buffer).await?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
output.write_all(&buffer[..read]).await?;
copied += read as u64;
}
output.flush().await?;
Ok::<_, std::io::Error>(())
}
.await;
if let Err(error) = copy_result {
let _ = tokio::fs::remove_file(&part_path).await;
return Err(error.into());
}
let actual_sha1 = hasher.digest().to_string();
if expected_size.is_some_and(|size| copied != size)
|| !actual_sha1.eq_ignore_ascii_case(expected_sha1)
{
let _ = tokio::fs::remove_file(&part_path).await;
return Ok(false);
}
if let Err(error) = fetch::finalize_download(&part_path, destination).await
{
let _ = tokio::fs::remove_file(&part_path).await;
return Err(error);
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::fetch::IoSemaphore;
use tokio::sync::Semaphore;
#[tokio::test]
async fn verified_copy_writes_and_hashes_in_one_pass() {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("source");
let destination = dir.path().join("nested/destination");
tokio::fs::write(&source, b"asset").await.unwrap();
let semaphore = IoSemaphore(Semaphore::new(1));
assert!(
copy_verified(
&source,
&destination,
Some("05fac94380a70241f23780e7aef62b190894238f"),
Some(5),
&semaphore,
)
.await
.unwrap()
);
assert_eq!(tokio::fs::read(&destination).await.unwrap(), b"asset");
assert!(!fetch::suffixed_path(&destination, ".part").exists());
}
#[tokio::test]
async fn failed_hash_removes_partial_and_destination() {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("source");
let destination = dir.path().join("destination");
tokio::fs::write(&source, b"asset").await.unwrap();
let semaphore = IoSemaphore(Semaphore::new(1));
assert!(
!copy_verified(
&source,
&destination,
Some("00"),
Some(5),
&semaphore
)
.await
.unwrap()
);
assert!(!destination.exists());
assert!(!fetch::suffixed_path(&destination, ".part").exists());
}
}