use std::io::Read;
use std::path::Path;
use std::time::Duration;
use runner_manager_domain::model::Arch;
use sha2::{Digest, Sha256};
use super::WslError;
use super::exec::{ChildInput, PipedInput};
use super::probe::{LinuxCommand, WslInvoker};
pub const DEFAULT_LINUX_DESTINATION: &str = "/usr/local/bin/runner-manager";
pub const MAX_ARCHIVE_BYTES: u64 = 256 * 1024 * 1024;
const EXTRACT_TIMEOUT: Duration = Duration::from_secs(300);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReleaseTarget {
triple: &'static str,
extension: &'static str,
binary: &'static str,
}
impl ReleaseTarget {
#[must_use]
pub fn triple(&self) -> &'static str {
self.triple
}
#[must_use]
pub fn extension(&self) -> &'static str {
self.extension
}
#[must_use]
pub fn binary(&self) -> &'static str {
self.binary
}
#[must_use]
pub fn asset_for(&self, version: &str) -> String {
format!(
"runner-manager-{version}-{}.{}",
self.triple, self.extension
)
}
}
pub fn linux_target(distribution: &str, arch: Arch) -> Result<ReleaseTarget, WslError> {
match arch {
Arch::X64 => Ok(ReleaseTarget {
triple: "x86_64-unknown-linux-gnu",
extension: "tar.gz",
binary: "runner-manager",
}),
Arch::Arm64 => Ok(ReleaseTarget {
triple: "aarch64-unknown-linux-gnu",
extension: "tar.gz",
binary: "runner-manager",
}),
Arch::Arm32 => Err(WslError::UnsupportedArchitecture {
distribution: distribution.to_string(),
reported: "32-bit ARM".to_string(),
}),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PublishedArtifact {
version: String,
asset: String,
digest: String,
}
impl PublishedArtifact {
#[must_use]
pub fn version(&self) -> &str {
&self.version
}
#[must_use]
pub fn asset(&self) -> &str {
&self.asset
}
#[must_use]
pub fn digest(&self) -> &str {
&self.digest
}
}
#[must_use]
pub fn parse_semantic_version(raw: &str) -> Option<(u64, u64, u64)> {
let mut parts = raw.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
let patch = parts.next()?.parse().ok()?;
if parts.next().is_some() {
return None;
}
Some((major, minor, patch))
}
#[must_use]
pub fn version_of_asset(name: &str, target: &ReleaseTarget) -> Option<String> {
let rest = name.strip_prefix("runner-manager-")?;
let rest = rest.strip_suffix(&format!(".{}", target.extension))?;
let version = rest.strip_suffix(&format!("-{}", target.triple))?;
parse_semantic_version(version).map(|_| version.to_string())
}
pub fn select_exact_release(
document: &str,
target: &ReleaseTarget,
version: &str,
) -> Result<PublishedArtifact, WslError> {
if parse_semantic_version(version).is_none() {
return Err(WslError::UnreadableChecksums {
detail: format!(
"`{version}` is not an exact `X.Y.Z` version, and this install selects an \
exact one rather than the newest"
),
});
}
let mut usable = 0_usize;
let mut matched: Vec<PublishedArtifact> = Vec::new();
for line in document.lines() {
let fields: Vec<&str> = line.trim_end_matches('\r').split_whitespace().collect();
let [digest, name] = fields[..] else { continue };
if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) {
continue;
}
usable += 1;
let name = name.strip_prefix('*').unwrap_or(name);
let Some(found) = version_of_asset(name, target) else {
continue;
};
if found != version {
continue;
}
matched.push(PublishedArtifact {
version: found,
asset: name.to_string(),
digest: digest.to_ascii_lowercase(),
});
}
if usable == 0 {
return Err(WslError::UnreadableChecksums {
detail: "the checksum document has no line that reads as \
'<64 hex digits><spaces><asset name>'; it is empty, truncated, or not a \
SHA256SUMS file at all"
.to_string(),
});
}
match matched.len() {
1 => Ok(matched.remove(0)),
0 => Err(WslError::NoSuchArtifact {
version: version.to_string(),
triple: target.triple.to_string(),
published: usable,
}),
count => Err(WslError::AmbiguousArtifact {
version: version.to_string(),
triple: target.triple.to_string(),
count,
}),
}
}
pub fn sha256_of_file(path: &Path) -> Result<String, WslError> {
let unreadable = |error: std::io::Error| WslError::UnreadableArchive {
path: path.to_path_buf(),
detail: error.to_string(),
};
let mut file = std::fs::File::open(path).map_err(unreadable)?;
let mut hasher = Sha256::new();
let mut buffer = vec![0_u8; 64 * 1024];
loop {
let read = file.read(&mut buffer).map_err(unreadable)?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
Ok(hex::encode(hasher.finalize()))
}
pub fn read_verified_archive(
path: &Path,
artifact: &PublishedArtifact,
) -> Result<Vec<u8>, WslError> {
let unreadable = |detail: String| WslError::UnreadableArchive {
path: path.to_path_buf(),
detail,
};
let metadata = std::fs::metadata(path).map_err(|error| unreadable(error.to_string()))?;
let too_large = |length: u64| {
unreadable(format!(
"it is {length} bytes, and this refuses to pipe anything larger than \
{MAX_ARCHIVE_BYTES}"
))
};
if metadata.len() > MAX_ARCHIVE_BYTES {
return Err(too_large(metadata.len()));
}
let bytes = std::fs::read(path).map_err(|error| unreadable(error.to_string()))?;
if bytes.len() as u64 > MAX_ARCHIVE_BYTES {
return Err(too_large(bytes.len() as u64));
}
let actual = hex::encode(Sha256::digest(&bytes));
if actual != artifact.digest {
return Err(WslError::DigestMismatch {
path: path.to_path_buf(),
expected: artifact.digest.clone(),
actual,
});
}
Ok(bytes)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinuxBinaryPath {
directory: String,
file_name: String,
}
impl LinuxBinaryPath {
pub fn parse(path: &str) -> Result<Self, WslError> {
let refuse = |reason: &str| {
Err(WslError::InvalidDestination {
path: path.to_string(),
reason: reason.to_string(),
})
};
if !path.starts_with('/') {
return refuse("it is not an absolute Linux path");
}
if path.chars().any(char::is_control) {
return refuse("it contains a control character");
}
let components: Vec<&str> = path.split('/').skip(1).collect();
if components.iter().any(|component| component.is_empty()) {
return refuse("it has an empty path component, or a trailing slash");
}
if components
.iter()
.any(|component| *component == "." || *component == "..")
{
return refuse("it contains a `.` or `..` component, which is not resolved here");
}
let Some((file_name, directory_parts)) = components.split_last() else {
return refuse("it names the root directory rather than a file");
};
Ok(Self {
directory: format!("/{}", directory_parts.join("/")),
file_name: (*file_name).to_string(),
})
}
#[must_use]
pub fn directory(&self) -> &str {
&self.directory
}
#[must_use]
pub fn file_name(&self) -> &str {
&self.file_name
}
#[must_use]
pub fn as_path(&self) -> String {
if self.directory == "/" {
format!("/{}", self.file_name)
} else {
format!("{}/{}", self.directory, self.file_name)
}
}
}
impl Default for LinuxBinaryPath {
fn default() -> Self {
Self::parse(DEFAULT_LINUX_DESTINATION)
.expect("the product's own default destination is a valid absolute path")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstalledBinary {
destination: String,
version: String,
asset: String,
}
impl InstalledBinary {
#[must_use]
pub fn destination(&self) -> &str {
&self.destination
}
#[must_use]
pub fn version(&self) -> &str {
&self.version
}
#[must_use]
pub fn asset(&self) -> &str {
&self.asset
}
}
#[derive(Debug)]
pub struct BinaryInstaller<'invoker> {
invoker: &'invoker WslInvoker<'invoker>,
distribution: String,
destination: LinuxBinaryPath,
staging_token: String,
}
impl<'invoker> BinaryInstaller<'invoker> {
#[must_use]
pub fn new(
invoker: &'invoker WslInvoker<'invoker>,
distribution: impl Into<String>,
destination: LinuxBinaryPath,
) -> Self {
Self {
invoker,
distribution: distribution.into(),
destination,
staging_token: uuid::Uuid::new_v4().simple().to_string(),
}
}
#[must_use]
pub fn with_staging_token(mut self, token: impl Into<String>) -> Self {
self.staging_token = token.into();
self
}
#[must_use]
pub fn staging_directory(&self) -> String {
let directory = self.destination.directory();
let separator = if directory.ends_with('/') { "" } else { "/" };
format!(
"{directory}{separator}.runner-manager-install-{}",
self.staging_token
)
}
pub fn install(
&self,
archive: &Path,
artifact: &PublishedArtifact,
target: &ReleaseTarget,
) -> Result<InstalledBinary, WslError> {
let bytes = read_verified_archive(archive, artifact)?;
let staging = self.staging_directory();
self.invoker.exec_ok(
"create a staging directory inside the distribution",
self.command("mkdir").args(["-m", "0700", staging.as_str()]),
)?;
let installed = self.stage_and_rename(&staging, bytes, artifact, target);
drop(
self.invoker
.exec(self.command("rm").args(["-rf", staging.as_str()])),
);
installed
}
fn stage_and_rename(
&self,
staging: &str,
bytes: Vec<u8>,
artifact: &PublishedArtifact,
target: &ReleaseTarget,
) -> Result<InstalledBinary, WslError> {
let member = archive_member(artifact, target);
let staged = format!("{staging}/{member}");
self.invoker.exec_ok(
"unpack the release archive inside the distribution",
self.command("tar")
.args([
"-xzf",
"-",
"-C",
staging,
"--no-same-owner",
member.as_str(),
])
.with_input(ChildInput::Piped(PipedInput::from_bytes(bytes)))
.with_timeout(EXTRACT_TIMEOUT),
)?;
self.invoker.exec_ok(
"make the unpacked binary executable",
self.command("chmod").args(["0755", staged.as_str()]),
)?;
let reported = self.invoker.exec_ok(
"read the unpacked binary's version",
self.command(staged.as_str()).args(["--version"]),
)?;
let reported = reported.stdout_text();
if !reports_version(&reported, artifact.version()) {
return Err(WslError::VersionMismatch {
expected: artifact.version().to_string(),
reported,
});
}
let destination = self.destination.as_path();
self.invoker.exec_ok(
"put the new binary in place",
self.command("mv")
.args(["-T", staged.as_str(), destination.as_str()]),
)?;
Ok(InstalledBinary {
destination,
version: artifact.version().to_string(),
asset: artifact.asset().to_string(),
})
}
fn command(&self, program: &str) -> LinuxCommand {
LinuxCommand::new(self.distribution.clone(), program)
}
}
#[must_use]
fn archive_member(artifact: &PublishedArtifact, target: &ReleaseTarget) -> String {
format!(
"runner-manager-{}-{}/{}",
artifact.version(),
target.triple(),
target.binary()
)
}
#[must_use]
fn reports_version(output: &str, version: &str) -> bool {
output
.split_whitespace()
.any(|token| token.trim_start_matches('v') == version)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use crate::wsl::exec::{CommandOutput, ScriptedRunner};
use crate::wsl::probe::WslExecutable;
const DIGEST_X64: &str = "1111111111111111111111111111111111111111111111111111111111111111";
const DIGEST_ARM: &str = "2222222222222222222222222222222222222222222222222222222222222222";
fn sums() -> String {
format!(
concat!(
"{x64} runner-manager-0.4.0-x86_64-unknown-linux-gnu.tar.gz\n",
"{arm} *runner-manager-0.4.0-aarch64-unknown-linux-gnu.tar.gz\n",
"3333333333333333333333333333333333333333333333333333333333333333 \
runner-manager-0.4.0-x86_64-pc-windows-msvc.zip\n",
"4444444444444444444444444444444444444444444444444444444444444444 \
runner-manager-0.3.2-x86_64-unknown-linux-gnu.tar.gz\n",
"5555555555555555555555555555555555555555555555555555555555555555 \
runner-manager-0.4.0-x86_64-unknown-linux-gnu.tar.gz.sig\n",
),
x64 = DIGEST_X64,
arm = DIGEST_ARM,
)
}
fn x64() -> ReleaseTarget {
linux_target("Ubuntu", Arch::X64).expect("x64 is published")
}
#[test]
fn linux_release_targets_match_the_published_matrix() {
assert_eq!(x64().triple(), "x86_64-unknown-linux-gnu");
assert_eq!(x64().extension(), "tar.gz");
assert_eq!(x64().binary(), "runner-manager");
let arm = linux_target("Ubuntu", Arch::Arm64).expect("arm64 is published");
assert_eq!(arm.triple(), "aarch64-unknown-linux-gnu");
assert_eq!(
x64().asset_for("0.4.0"),
"runner-manager-0.4.0-x86_64-unknown-linux-gnu.tar.gz"
);
}
#[test]
fn thirty_two_bit_arm_has_no_artifact_and_is_refused_rather_than_guessed() {
let error = linux_target("Ubuntu", Arch::Arm32).expect_err("nothing is published");
assert!(
matches!(error, WslError::UnsupportedArchitecture { .. }),
"{error:?}"
);
}
#[test]
fn the_exact_version_and_architecture_are_selected_out_of_a_real_document() {
let artifact = select_exact_release(&sums(), &x64(), "0.4.0").expect("published");
assert_eq!(artifact.version(), "0.4.0");
assert_eq!(
artifact.asset(),
"runner-manager-0.4.0-x86_64-unknown-linux-gnu.tar.gz"
);
assert_eq!(artifact.digest(), DIGEST_X64);
}
#[test]
fn the_star_form_of_a_checksum_line_is_accepted_as_sha256sum_accepts_it() {
let arm = linux_target("Ubuntu", Arch::Arm64).expect("published");
let artifact = select_exact_release(&sums(), &arm, "0.4.0").expect("published");
assert_eq!(artifact.digest(), DIGEST_ARM);
assert!(
!artifact.asset().starts_with('*'),
"the `*` marks a binary read, and is not part of the name"
);
}
#[test]
fn a_signature_file_is_not_an_archive() {
assert_eq!(
version_of_asset(
"runner-manager-0.4.0-x86_64-unknown-linux-gnu.tar.gz.sig",
&x64()
),
None
);
}
#[test]
fn a_newer_published_version_is_not_accepted_when_an_exact_one_was_asked_for() {
let artifact = select_exact_release(&sums(), &x64(), "0.3.2").expect("published");
assert_eq!(artifact.version(), "0.3.2");
}
#[test]
fn a_version_the_release_does_not_publish_says_how_many_assets_it_has() {
let error = select_exact_release(&sums(), &x64(), "9.9.9").expect_err("not published");
let WslError::NoSuchArtifact {
version,
triple,
published,
} = &error
else {
panic!("unexpected error: {error:?}");
};
assert_eq!(version, "9.9.9");
assert_eq!(triple, "x86_64-unknown-linux-gnu");
assert_eq!(*published, 5);
}
#[test]
fn two_archives_for_one_target_refuse_rather_than_guess() {
let document = format!(
"{DIGEST_X64} runner-manager-0.4.0-x86_64-unknown-linux-gnu.tar.gz\n\
{DIGEST_ARM} runner-manager-0.4.0-x86_64-unknown-linux-gnu.tar.gz\n"
);
let error = select_exact_release(&document, &x64(), "0.4.0").expect_err("ambiguous");
assert!(
matches!(error, WslError::AmbiguousArtifact { count: 2, .. }),
"{error:?}"
);
}
#[test]
fn a_document_that_is_not_a_checksum_file_is_told_apart_from_a_missing_row() {
let error = select_exact_release("<html>404</html>", &x64(), "0.4.0")
.expect_err("not a checksum document");
assert!(
matches!(error, WslError::UnreadableChecksums { .. }),
"{error:?}"
);
}
#[test]
fn an_inexact_version_is_refused_before_the_document_is_read() {
for version in ["0.4", "0.4.0-rc.1", "latest", "v0.4.0", "0.4.0+build", ""] {
let error = select_exact_release(&sums(), &x64(), version)
.expect_err("an inexact version must be refused");
assert!(
matches!(error, WslError::UnreadableChecksums { .. }),
"{version:?} produced the wrong refusal: {error:?}"
);
}
assert_eq!(
select_exact_release(&sums(), &x64(), "0.4.0")
.expect("0.4.0 is published")
.version(),
"0.4.0"
);
}
#[test]
fn the_archive_member_is_the_path_the_release_really_packages() {
let artifact = select_exact_release(&sums(), &x64(), "0.4.0").expect("published");
let member = archive_member(&artifact, &x64());
let stem = artifact
.asset()
.strip_suffix(&format!(".{}", x64().extension()))
.expect("the asset name carries the target's extension");
assert_eq!(member, format!("{stem}/{}", x64().binary()));
assert_eq!(
member,
"runner-manager-0.4.0-x86_64-unknown-linux-gnu/runner-manager"
);
}
fn write_archive(directory: &Path, bytes: &[u8]) -> (PathBuf, String) {
let path = directory.join("archive.tar.gz");
std::fs::write(&path, bytes).expect("write the archive");
let digest = sha256_of_file(&path).expect("hash it");
(path, digest)
}
#[test]
fn the_digest_is_the_one_sha256sum_would_print() {
let directory = tempfile::tempdir().expect("a temporary directory");
let (_, digest) = write_archive(directory.path(), b"");
assert_eq!(
digest,
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn an_archive_whose_digest_does_not_match_is_never_returned_to_be_piped() {
let directory = tempfile::tempdir().expect("a temporary directory");
let (path, _) = write_archive(directory.path(), b"not the published bytes");
let artifact = PublishedArtifact {
version: "0.4.0".to_string(),
asset: "runner-manager-0.4.0-x86_64-unknown-linux-gnu.tar.gz".to_string(),
digest: DIGEST_X64.to_string(),
};
let error = read_verified_archive(&path, &artifact).expect_err("mismatch");
let WslError::DigestMismatch {
expected, actual, ..
} = &error
else {
panic!("unexpected error: {error:?}");
};
assert_eq!(expected, DIGEST_X64);
assert_ne!(actual, DIGEST_X64);
}
#[test]
fn the_default_destination_is_the_one_the_linux_service_already_assumes() {
let destination = LinuxBinaryPath::default();
assert_eq!(destination.as_path(), DEFAULT_LINUX_DESTINATION);
assert_eq!(destination.directory(), "/usr/local/bin");
assert_eq!(destination.file_name(), "runner-manager");
}
#[test]
fn a_destination_that_would_move_the_staging_directory_elsewhere_is_refused() {
for path in [
"usr/local/bin/runner-manager",
"/usr/local/bin/",
"/usr/local//bin/runner-manager",
"/usr/local/bin/../../tmp/runner-manager",
"/usr/local/bin/./runner-manager",
"/",
"/usr/local/bin/runner\nmanager",
] {
assert!(
LinuxBinaryPath::parse(path).is_err(),
"{path:?} should be refused"
);
}
}
#[test]
fn a_destination_at_the_root_still_stages_beside_itself() {
let destination = LinuxBinaryPath::parse("/runner-manager").expect("valid");
assert_eq!(destination.directory(), "/");
assert_eq!(destination.as_path(), "/runner-manager");
let runner = ScriptedRunner::new();
let executable = WslExecutable::at("wsl.exe");
let invoker = WslInvoker::new(&runner, &executable);
let installer =
BinaryInstaller::new(&invoker, "Ubuntu", destination).with_staging_token("token");
assert_eq!(
installer.staging_directory(),
"/.runner-manager-install-token"
);
}
struct Fixture {
directory: tempfile::TempDir,
archive: PathBuf,
artifact: PublishedArtifact,
}
fn fixture() -> Fixture {
let directory = tempfile::tempdir().expect("a temporary directory");
let (archive, digest) = write_archive(directory.path(), b"pretend this is a tar.gz");
let artifact = PublishedArtifact {
version: "0.4.0".to_string(),
asset: "runner-manager-0.4.0-x86_64-unknown-linux-gnu.tar.gz".to_string(),
digest,
};
Fixture {
directory,
archive,
artifact,
}
}
fn healthy_runner() -> ScriptedRunner {
ScriptedRunner::new().always(
"--version",
CommandOutput::exited(0, "runner-manager 0.4.0\n", ""),
)
}
#[test]
fn a_successful_install_runs_exactly_the_expected_argument_vectors_in_order() {
let fixture = fixture();
let runner = healthy_runner();
let executable = WslExecutable::at("wsl.exe");
let invoker = WslInvoker::new(&runner, &executable);
let installed = BinaryInstaller::new(&invoker, "Ubuntu", LinuxBinaryPath::default())
.with_staging_token("token")
.install(&fixture.archive, &fixture.artifact, &x64())
.expect("the scripted distribution accepts every step");
assert_eq!(installed.destination(), DEFAULT_LINUX_DESTINATION);
assert_eq!(installed.version(), "0.4.0");
let staging = "/usr/local/bin/.runner-manager-install-token";
let member = "runner-manager-0.4.0-x86_64-unknown-linux-gnu/runner-manager";
let staged = format!("{staging}/{member}");
let staged = staged.as_str();
let expected: Vec<Vec<String>> = vec![
vec!["mkdir", "-m", "0700", staging],
vec!["tar", "-xzf", "-", "-C", staging, "--no-same-owner", member],
vec!["chmod", "0755", staged],
vec![staged, "--version"],
vec!["mv", "-T", staged, DEFAULT_LINUX_DESTINATION],
vec!["rm", "-rf", staging],
]
.into_iter()
.map(|step| {
let mut argv = vec![
"--distribution".to_string(),
"Ubuntu".to_string(),
"--user".to_string(),
"root".to_string(),
"--exec".to_string(),
];
argv.extend(step.into_iter().map(str::to_string));
argv
})
.collect();
let actual: Vec<Vec<String>> = runner
.recorded()
.into_iter()
.map(|request| request.arguments)
.collect();
assert_eq!(actual, expected);
drop(fixture.directory);
}
#[test]
fn the_staging_directory_is_beside_the_destination_so_the_rename_is_atomic() {
let runner = healthy_runner();
let executable = WslExecutable::at("wsl.exe");
let invoker = WslInvoker::new(&runner, &executable);
let installer = BinaryInstaller::new(&invoker, "Ubuntu", LinuxBinaryPath::default())
.with_staging_token("token");
let staging = installer.staging_directory();
assert!(staging.starts_with("/usr/local/bin/"));
assert_eq!(
staging.rfind('/'),
Some("/usr/local/bin".len()),
"the staging directory must be a direct child of the destination's directory: \
{staging}"
);
}
#[test]
fn a_digest_mismatch_never_reaches_the_distribution_at_all() {
let fixture = fixture();
let mut wrong = fixture.artifact.clone();
wrong.digest = DIGEST_X64.to_string();
let runner = healthy_runner();
let executable = WslExecutable::at("wsl.exe");
let invoker = WslInvoker::new(&runner, &executable);
let error = BinaryInstaller::new(&invoker, "Ubuntu", LinuxBinaryPath::default())
.install(&fixture.archive, &wrong, &x64())
.expect_err("the archive is not the published one");
assert!(
matches!(error, WslError::DigestMismatch { .. }),
"{error:?}"
);
assert_eq!(
runner.call_count(),
0,
"nothing may run in the distribution: {:?}",
runner.command_lines()
);
}
#[test]
fn a_failed_extraction_preserves_the_destination_and_removes_the_staging_directory() {
let fixture = fixture();
let runner = healthy_runner().always(
"--exec tar",
CommandOutput::exited(2, "", "gzip: stdin: not in gzip format\n"),
);
let executable = WslExecutable::at("wsl.exe");
let invoker = WslInvoker::new(&runner, &executable);
let error = BinaryInstaller::new(&invoker, "Ubuntu", LinuxBinaryPath::default())
.with_staging_token("token")
.install(&fixture.archive, &fixture.artifact, &x64())
.expect_err("tar refused");
assert!(error.to_string().contains("not in gzip format"), "{error}");
let lines = runner.command_lines();
assert!(
lines.iter().all(|line| !line.contains("--exec mv")),
"the destination must not be touched: {lines:?}"
);
assert!(
lines
.iter()
.any(|line| line
.contains("--exec rm -rf /usr/local/bin/.runner-manager-install-token")),
"the staging directory must be removed: {lines:?}"
);
}
#[test]
fn a_binary_reporting_a_different_version_is_never_renamed_into_place() {
let fixture = fixture();
let runner = ScriptedRunner::new().always(
"--version",
CommandOutput::exited(0, "runner-manager 0.4.10\n", ""),
);
let executable = WslExecutable::at("wsl.exe");
let invoker = WslInvoker::new(&runner, &executable);
let error = BinaryInstaller::new(&invoker, "Ubuntu", LinuxBinaryPath::default())
.install(&fixture.archive, &fixture.artifact, &x64())
.expect_err("0.4.10 is not 0.4.0");
let WslError::VersionMismatch { expected, reported } = &error else {
panic!("unexpected error: {error:?}");
};
assert_eq!(expected, "0.4.0");
assert!(reported.contains("0.4.10"));
assert!(
runner
.command_lines()
.iter()
.all(|line| !line.contains("--exec mv")),
"the destination must not be touched"
);
}
#[test]
fn a_version_check_matches_whole_tokens_rather_than_prefixes() {
assert!(reports_version("runner-manager 0.4.0", "0.4.0"));
assert!(reports_version("runner-manager v0.4.0", "0.4.0"));
assert!(!reports_version("runner-manager 0.4.10", "0.4.0"));
assert!(!reports_version("runner-manager 10.4.0", "0.4.0"));
assert!(!reports_version("", "0.4.0"));
}
#[test]
fn the_archive_is_piped_rather_than_written_into_the_distribution() {
let fixture = fixture();
let runner = healthy_runner();
let executable = WslExecutable::at("wsl.exe");
let invoker = WslInvoker::new(&runner, &executable);
BinaryInstaller::new(&invoker, "Ubuntu", LinuxBinaryPath::default())
.with_staging_token("token")
.install(&fixture.archive, &fixture.artifact, &x64())
.expect("installed");
let piped = runner.piped_input();
assert_eq!(
piped,
std::fs::read(&fixture.archive).expect("the archive is readable"),
"the whole archive should have gone through the pipe"
);
for request in runner.recorded() {
assert!(
!request
.arguments
.iter()
.any(|argument| argument.contains(".tar.gz")),
"no step may name an archive file inside the distribution: {:?}",
request.arguments
);
}
}
}