1use std::{
2 fs::{self, OpenOptions},
3 io::{BufReader, Read, Write},
4 path::{Path, PathBuf},
5 thread::sleep,
6 time::{Duration, Instant},
7};
8
9use color_eyre::eyre::{eyre, Report};
10use flate2::read::GzDecoder;
11use reqwest::{blocking::Client, redirect::Policy, Url};
12use sha2::{Digest, Sha256};
13use tar::Archive;
14use tempfile::NamedTempFile;
15
16use super::{
17 Config, ConfigZcashdBinarySource, ZcashdReleaseManifest, EMBEDDED_ZCASHD_RELEASE_MANIFEST,
18};
19use crate::components::zcashd_compat::supervisor::is_command_resolvable;
20
21const MANAGED_DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(10 * 60);
23
24const MANAGED_DOWNLOAD_MAX_BYTES: u64 = 1024 * 1024 * 1024;
33const INSTALL_LOCK_WAIT_TIMEOUT: Duration = Duration::from_secs(15 * 60);
35const LEGACY_INSTALL_LOCK_STALE_AFTER: Duration = Duration::from_secs(30);
37const INSTALL_LOCK_RETRY_DELAY: Duration = Duration::from_millis(250);
38
39#[derive(Clone, Debug, Eq, PartialEq)]
41pub enum ZcashdBinarySource {
42 Path(PathBuf),
44 Embedded,
46}
47
48pub fn zcashd_target_triple() -> Option<&'static str> {
50 let target = match (std::env::consts::OS, std::env::consts::ARCH) {
51 ("linux", "x86_64") => Some("x86_64-pc-linux-gnu"),
52 _ => None,
53 }?;
54
55 EMBEDDED_ZCASHD_RELEASE_MANIFEST
56 .artifact_for_target(target)
57 .map(|_| target)
58}
59
60pub fn effective_zcashd_source(config: &Config) -> Result<ZcashdBinarySource, Report> {
62 if let Some(path) = config.zcashd_path.clone() {
63 return Ok(ZcashdBinarySource::Path(path));
64 }
65
66 match config.zcashd_source {
67 ConfigZcashdBinarySource::Embedded => Ok(ZcashdBinarySource::Embedded),
68 ConfigZcashdBinarySource::Path => Err(eyre!(
69 "zcashd_compat.zcashd_source=path requires zcashd_compat.zcashd_path to be set"
70 )),
71 }
72}
73
74pub fn resolve_zcashd_binary_path(
76 config: &Config,
77 state_cache_dir: &Path,
78) -> Result<PathBuf, Report> {
79 match effective_zcashd_source(config)? {
80 ZcashdBinarySource::Path(path) => {
81 if !is_command_resolvable(&path) {
82 return Err(eyre!(
83 "zcashd-compat mode could not resolve zcashd_path={}",
84 path.display()
85 ));
86 }
87 Ok(path)
88 }
89 ZcashdBinarySource::Embedded => resolve_managed_zcashd_binary(state_cache_dir),
90 }
91}
92
93pub fn resolve_managed_zcashd_binary(state_cache_dir: &Path) -> Result<PathBuf, Report> {
95 resolve_managed_zcashd_binary_from_manifest(&EMBEDDED_ZCASHD_RELEASE_MANIFEST, state_cache_dir)
96}
97
98#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
104pub(super) fn managed_zcashd_binary_path(state_cache_dir: &Path) -> Option<PathBuf> {
105 let target = zcashd_target_triple()?;
106
107 Some(
108 managed_zcashd_cache_dir(
109 state_cache_dir,
110 &EMBEDDED_ZCASHD_RELEASE_MANIFEST.release_tag,
111 target,
112 )
113 .join("zcashd"),
114 )
115}
116
117#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
130pub(super) fn cached_managed_zcashd_binary_is_present(state_cache_dir: &Path) -> Option<bool> {
131 let target = zcashd_target_triple()?;
132 let artifact = EMBEDDED_ZCASHD_RELEASE_MANIFEST.artifact_for_target(target)?;
133 let binary_path = managed_zcashd_binary_path(state_cache_dir)?;
134 let provenance_path = binary_path.with_file_name("zcashd.sha256");
135
136 if !binary_path.is_file() {
137 return Some(false);
138 }
139
140 let content = fs::read_to_string(provenance_path).unwrap_or_default();
141 let recorded_archive = content
142 .lines()
143 .find_map(|line| line.strip_prefix("archive_sha256:"))
144 .map(str::trim);
145
146 Some(
147 recorded_archive.is_some_and(|recorded| {
148 recorded.eq_ignore_ascii_case(&artifact.runtime_archive_sha256)
149 }),
150 )
151}
152
153fn resolve_managed_zcashd_binary_from_manifest(
154 manifest: &ZcashdReleaseManifest,
155 state_cache_dir: &Path,
156) -> Result<PathBuf, Report> {
157 let target = zcashd_target_triple().ok_or_else(|| {
158 eyre!(
159 "zcashd-compat managed downloads are unsupported for this platform ({}/{})",
160 std::env::consts::OS,
161 std::env::consts::ARCH
162 )
163 })?;
164
165 let artifact = manifest.artifact_for_target(target).ok_or_else(|| {
166 eyre!(
167 "no managed zcashd release is configured for target {target}; \
168 set zcashd_compat.zcashd_path to a local zcashd binary"
169 )
170 })?;
171
172 let cache_dir = managed_zcashd_cache_dir(state_cache_dir, &manifest.release_tag, target);
173 fs::create_dir_all(&cache_dir)?;
174
175 let binary_path = cache_dir.join("zcashd");
176 let provenance_path = cache_dir.join("zcashd.sha256");
177 if provenance_matches(
178 &provenance_path,
179 &artifact.runtime_archive_sha256,
180 &binary_path,
181 )? {
182 return Ok(binary_path);
183 }
184
185 let _lock = acquire_lock(
186 &cache_dir.join(".install.lock"),
187 INSTALL_LOCK_WAIT_TIMEOUT,
188 LEGACY_INSTALL_LOCK_STALE_AFTER,
189 )?;
190
191 if provenance_matches(
193 &provenance_path,
194 &artifact.runtime_archive_sha256,
195 &binary_path,
196 )? {
197 return Ok(binary_path);
198 }
199
200 let mut archive_temp = NamedTempFile::new_in(&cache_dir)?;
201 download_archive(&artifact.runtime_archive_url, archive_temp.as_file_mut())?;
202 archive_temp.as_file_mut().sync_all()?;
203
204 let archive_sha256 = sha256_hex_file(archive_temp.path())?;
205 if !archive_sha256.eq_ignore_ascii_case(&artifact.runtime_archive_sha256) {
206 return Err(eyre!(
207 "managed zcashd archive hash mismatch for target {target}: expected {}, got {archive_sha256}",
208 artifact.runtime_archive_sha256
209 ));
210 }
211
212 let extracted_temp = NamedTempFile::new_in(&cache_dir)?;
213 extract_archive_member_to_path(
214 archive_temp.path(),
215 &artifact.runtime_archive_member_binary_path,
216 extracted_temp.path(),
217 )?;
218 make_executable(extracted_temp.path())?;
219 let binary_sha256 = sha256_hex_file(extracted_temp.path())?;
220 extracted_temp.persist(&binary_path).map_err(|err| {
221 eyre!(
222 "failed to persist managed zcashd binary {}: {}",
223 binary_path.display(),
224 err.error
225 )
226 })?;
227 fs::write(
228 &provenance_path,
229 provenance_content(&artifact.runtime_archive_sha256, &binary_sha256),
230 )?;
231
232 Ok(binary_path)
233}
234
235fn managed_zcashd_cache_dir(state_cache_dir: &Path, release_tag: &str, target: &str) -> PathBuf {
236 state_cache_dir
237 .join("zcashd-compat")
238 .join("bin")
239 .join(release_tag)
240 .join(target)
241}
242
243fn download_archive(url: &str, out: &mut fs::File) -> Result<(), Report> {
250 let parsed =
251 Url::parse(url).map_err(|err| eyre!("invalid managed zcashd URL '{url}': {err}"))?;
252 if parsed.scheme() != "https" {
253 #[cfg(test)]
254 let localhost_http = parsed.scheme() == "http"
255 && parsed
256 .host_str()
257 .map(|host| host == "127.0.0.1" || host == "localhost")
258 .unwrap_or(false);
259 #[cfg(not(test))]
260 let localhost_http = false;
261
262 if !localhost_http {
263 return Err(eyre!("managed zcashd URL must use https: {url}"));
264 }
265 }
266
267 let https_only_redirects = Policy::custom(|attempt| {
272 if attempt.previous().len() > 5 {
273 attempt.error("too many redirects")
274 } else if attempt.url().scheme() != "https" {
275 attempt.error("managed zcashd download redirected to a non-https URL")
276 } else {
277 attempt.follow()
278 }
279 });
280 let client = Client::builder()
281 .redirect(https_only_redirects)
282 .timeout(MANAGED_DOWNLOAD_TIMEOUT)
283 .build()
284 .map_err(|err| eyre!("failed building managed zcashd HTTP client: {err}"))?;
285 let mut response = client
286 .get(parsed)
287 .send()
288 .map_err(|err| eyre!("failed downloading managed zcashd archive: {err}"))?;
289 if !response.status().is_success() {
290 return Err(eyre!(
291 "managed zcashd download failed with HTTP status {}",
292 response.status()
293 ));
294 }
295 if response.url().scheme() != "https" {
296 #[cfg(test)]
297 let localhost_http = response.url().scheme() == "http"
298 && response
299 .url()
300 .host_str()
301 .map(|host| host == "127.0.0.1" || host == "localhost")
302 .unwrap_or(false);
303 #[cfg(not(test))]
304 let localhost_http = false;
305
306 if localhost_http {
307 } else {
309 return Err(eyre!(
310 "managed zcashd download redirected to non-https URL: {}",
311 response.url()
312 ));
313 }
314 }
315
316 let mut limited = (&mut response).take(MANAGED_DOWNLOAD_MAX_BYTES.saturating_add(1));
321 let written = std::io::copy(&mut limited, out)
322 .map_err(|err| eyre!("failed writing managed zcashd archive to cache: {err}"))?;
323 if written > MANAGED_DOWNLOAD_MAX_BYTES {
324 return Err(eyre!(
325 "managed zcashd archive exceeds the maximum allowed size of \
326 {MANAGED_DOWNLOAD_MAX_BYTES} bytes; aborting download"
327 ));
328 }
329 Ok(())
330}
331
332fn extract_archive_member_to_path(
337 archive_path: &Path,
338 member_path: &str,
339 destination: &Path,
340) -> Result<(), Report> {
341 let file = fs::File::open(archive_path)?;
342 let reader = BufReader::new(file);
343 let decoder = GzDecoder::new(reader);
344 let mut archive = Archive::new(decoder);
345 let requested = normalize_member_path(member_path);
346
347 for entry in archive.entries()? {
348 let mut entry = entry?;
349 let entry_path = entry.path()?;
350 let candidate = normalize_member_path(entry_path.to_string_lossy().as_ref());
351 if candidate == requested {
352 entry
353 .unpack(destination)
354 .map_err(|err| eyre!("failed extracting managed zcashd binary: {err}"))?;
355 return Ok(());
356 }
357 }
358
359 Err(eyre!(
360 "managed zcashd archive does not contain expected member path '{}'",
361 member_path
362 ))
363}
364
365fn normalize_member_path(path: &str) -> String {
370 path.trim_start_matches("./").to_string()
371}
372
373fn provenance_content(archive_sha256: &str, binary_sha256: &str) -> String {
376 format!("archive_sha256:{archive_sha256}\nbinary_sha256:{binary_sha256}\n")
377}
378
379fn provenance_matches(
388 path: &Path,
389 expected_archive_sha256: &str,
390 binary_path: &Path,
391) -> Result<bool, Report> {
392 if !path.is_file() || !binary_path.is_file() {
393 return Ok(false);
394 }
395
396 let content = fs::read_to_string(path).unwrap_or_default();
397 let mut recorded_archive = None;
398 let mut recorded_binary = None;
399 for line in content.lines() {
400 if let Some(value) = line.strip_prefix("archive_sha256:") {
401 recorded_archive = Some(value.trim().to_string());
402 } else if let Some(value) = line.strip_prefix("binary_sha256:") {
403 recorded_binary = Some(value.trim().to_string());
404 }
405 }
406
407 let (Some(recorded_archive), Some(recorded_binary)) = (recorded_archive, recorded_binary)
408 else {
409 return Ok(false);
412 };
413
414 if !recorded_archive.eq_ignore_ascii_case(expected_archive_sha256) {
415 return Ok(false);
416 }
417
418 Ok(sha256_hex_file(binary_path)?.eq_ignore_ascii_case(&recorded_binary))
419}
420
421fn sha256_hex_file(path: &Path) -> Result<String, Report> {
423 let mut file = fs::File::open(path)?;
424 let mut hasher = Sha256::new();
425 let mut buf = [0u8; 16 * 1024];
426
427 loop {
428 let n = file.read(&mut buf)?;
429 if n == 0 {
430 break;
431 }
432 hasher.update(&buf[..n]);
433 }
434
435 let digest = hasher.finalize();
436 Ok(hex::encode(digest))
437}
438
439fn make_executable(path: &Path) -> Result<(), Report> {
443 #[cfg(unix)]
444 {
445 use std::os::unix::fs::PermissionsExt;
446
447 let mut permissions = fs::metadata(path)?.permissions();
448 permissions.set_mode(0o755);
449 fs::set_permissions(path, permissions)?;
450 }
451
452 Ok(())
453}
454
455struct InstallLock {
456 path: PathBuf,
457}
458
459impl Drop for InstallLock {
460 fn drop(&mut self) {
461 let _ = fs::remove_file(&self.path);
462 }
463}
464
465fn acquire_lock(
470 lock_path: &Path,
471 wait_timeout: Duration,
472 stale_after: Duration,
473) -> Result<InstallLock, Report> {
474 let started = Instant::now();
475 loop {
476 match OpenOptions::new()
477 .create_new(true)
478 .write(true)
479 .open(lock_path)
480 {
481 Ok(mut file) => {
482 if let Err(error) = write_install_lock_owner(&mut file, lock_path) {
483 let _ = fs::remove_file(lock_path);
484 return Err(error);
485 }
486
487 return Ok(InstallLock {
488 path: lock_path.to_path_buf(),
489 });
490 }
491 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
492 if remove_stale_lock(lock_path, stale_after)? {
493 continue;
494 }
495
496 if started.elapsed() >= wait_timeout {
497 return Err(eyre!(
498 "timed out after {} seconds waiting for managed zcashd installation lock: {}",
499 wait_timeout.as_secs(),
500 lock_path.display(),
501 ));
502 }
503
504 sleep(INSTALL_LOCK_RETRY_DELAY);
505 }
506 Err(err) => {
507 return Err(eyre!(
508 "failed to create managed zcashd installation lock {}: {err}",
509 lock_path.display()
510 ))
511 }
512 }
513 }
514}
515
516fn write_install_lock_owner(file: &mut fs::File, lock_path: &Path) -> Result<(), Report> {
517 writeln!(file, "pid={}", std::process::id()).map_err(|err| {
518 eyre!(
519 "failed to write managed zcashd installation lock {}: {err}",
520 lock_path.display()
521 )
522 })?;
523 file.sync_all().map_err(|err| {
524 eyre!(
525 "failed to sync managed zcashd installation lock {}: {err}",
526 lock_path.display()
527 )
528 })
529}
530
531fn remove_stale_lock(lock_path: &Path, stale_after: Duration) -> Result<bool, Report> {
532 let content = match fs::read_to_string(lock_path) {
533 Ok(content) => content,
534 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(true),
535 Err(err) => {
536 return Err(eyre!(
537 "failed to read managed zcashd installation lock {}: {err}",
538 lock_path.display()
539 ))
540 }
541 };
542
543 if let Some(pid) = lock_owner_pid(&content) {
544 if process_is_running(pid) {
545 return Ok(false);
546 }
547 } else if !lock_file_is_older_than(lock_path, stale_after)? {
548 return Ok(false);
549 }
550
551 match fs::remove_file(lock_path) {
552 Ok(()) => Ok(true),
553 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(true),
554 Err(err) => Err(eyre!(
555 "failed to remove stale managed zcashd installation lock {}: {err}",
556 lock_path.display()
557 )),
558 }
559}
560
561fn lock_owner_pid(content: &str) -> Option<u32> {
562 content.lines().find_map(|line| {
563 line.strip_prefix("pid=")
564 .and_then(|pid| pid.trim().parse().ok())
565 })
566}
567
568fn lock_file_is_older_than(lock_path: &Path, age: Duration) -> Result<bool, Report> {
569 let metadata = match fs::metadata(lock_path) {
570 Ok(metadata) => metadata,
571 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(true),
572 Err(err) => {
573 return Err(eyre!(
574 "failed to inspect managed zcashd installation lock {}: {err}",
575 lock_path.display()
576 ))
577 }
578 };
579
580 let Ok(modified_age) = metadata.modified()?.elapsed() else {
581 return Ok(false);
582 };
583
584 Ok(modified_age >= age)
585}
586
587#[cfg(unix)]
588fn process_is_running(pid: u32) -> bool {
589 use nix::{errno::Errno, sys::signal::kill, unistd::Pid};
590
591 let Ok(pid) = i32::try_from(pid) else {
592 return false;
593 };
594
595 match kill(Pid::from_raw(pid), None) {
596 Ok(()) => true,
597 Err(Errno::EPERM) => true,
598 Err(Errno::ESRCH) => false,
599 Err(_) => false,
600 }
601}
602
603#[cfg(not(unix))]
604fn process_is_running(_pid: u32) -> bool {
605 true
606}
607
608#[cfg(test)]
609mod tests {
610 use std::{
611 io::{Read, Write},
612 net::TcpListener,
613 thread,
614 time::Duration,
615 };
616
617 use flate2::{write::GzEncoder, Compression};
618 use tar::Builder;
619 use tempfile::tempdir;
620
621 use super::{
622 acquire_lock, effective_zcashd_source, normalize_member_path, provenance_content,
623 provenance_matches, resolve_managed_zcashd_binary_from_manifest, sha256_hex_file,
624 zcashd_target_triple, Config, ZcashdBinarySource,
625 };
626 use crate::components::zcashd_compat::{
627 ConfigZcashdBinarySource, ZcashdReleaseArtifact, ZcashdReleaseManifest,
628 EMBEDDED_ZCASHD_RELEASE_MANIFEST,
629 };
630
631 #[test]
632 fn explicit_zcashd_path_overrides_embedded_source() {
633 let config = Config {
634 zcashd_source: ConfigZcashdBinarySource::Embedded,
635 zcashd_path: Some("/usr/local/bin/zcashd".into()),
636 ..Default::default()
637 };
638
639 assert_eq!(
640 effective_zcashd_source(&config).expect("source should resolve"),
641 ZcashdBinarySource::Path("/usr/local/bin/zcashd".into())
642 );
643 }
644
645 #[test]
646 fn path_source_requires_explicit_path() {
647 let config = Config {
648 zcashd_source: ConfigZcashdBinarySource::Path,
649 zcashd_path: None,
650 ..Default::default()
651 };
652
653 let error = effective_zcashd_source(&config).expect_err("path source should fail");
654 assert!(error.to_string().contains("zcashd_source=path"));
655 }
656
657 #[test]
658 fn target_triple_is_configured_or_none() {
659 if let Some(target) = zcashd_target_triple() {
660 assert_eq!(
661 target, "x86_64-pc-linux-gnu",
662 "managed zcashd downloads are only published for x86_64"
663 );
664 assert!(
665 EMBEDDED_ZCASHD_RELEASE_MANIFEST
666 .artifact_for_target(target)
667 .is_some(),
668 "managed target triple is not configured in embedded manifest: {target}"
669 );
670 }
671 }
672
673 #[test]
674 fn normalize_member_path_strips_dot_prefix() {
675 assert_eq!(normalize_member_path("./bin/zcashd"), "bin/zcashd");
676 assert_eq!(normalize_member_path("bin/zcashd"), "bin/zcashd");
677 }
678
679 #[test]
680 fn provenance_matches_verifies_marker_and_binary_digest() {
681 let temp = tempdir().expect("tempdir should exist");
682 let path = temp.path().join("sha.txt");
683 let binary_path = temp.path().join("zcashd");
684
685 assert!(
686 !provenance_matches(&path, "abc", &binary_path).expect("missing file should not match"),
687 "missing provenance should not match"
688 );
689
690 std::fs::write(&binary_path, b"binary contents").expect("binary file should write");
691 let binary_sha256 =
692 sha256_hex_file(&binary_path).expect("binary digest should be computable");
693
694 std::fs::write(&path, provenance_content("abc", &binary_sha256))
695 .expect("provenance file should write");
696 assert!(
697 provenance_matches(&path, "abc", &binary_path)
698 .expect("matching provenance should succeed"),
699 "written provenance should match"
700 );
701 assert!(
702 !provenance_matches(&path, "def", &binary_path)
703 .expect("mismatched provenance should succeed"),
704 "different expected archive hash should not match"
705 );
706
707 std::fs::write(&path, "abc\n").expect("provenance file should write");
709 assert!(
710 !provenance_matches(&path, "abc", &binary_path)
711 .expect("legacy provenance should be readable"),
712 "legacy archive-only provenance should not match"
713 );
714
715 std::fs::write(&path, provenance_content("abc", &binary_sha256))
717 .expect("provenance file should write");
718 std::fs::write(&binary_path, b"tampered contents").expect("binary file should write");
719 assert!(
720 !provenance_matches(&path, "abc", &binary_path)
721 .expect("tampered binary check should succeed"),
722 "a tampered cached binary should not match"
723 );
724 }
725
726 #[cfg(unix)]
727 #[test]
728 fn acquire_lock_replaces_dead_owner_lock() {
729 let temp = tempdir().expect("tempdir should exist");
730 let lock_path = temp.path().join(".install.lock");
731 std::fs::write(&lock_path, "pid=4294967295\n").expect("lock file should write");
732
733 let lock = acquire_lock(&lock_path, Duration::ZERO, Duration::ZERO)
734 .expect("dead owner lock should recover");
735 let content = std::fs::read_to_string(&lock_path).expect("lock file should be readable");
736
737 assert!(
738 content.contains(&format!("pid={}\n", std::process::id())),
739 "lock file should contain current process owner: {content}"
740 );
741
742 drop(lock);
743 assert!(
744 !lock_path.exists(),
745 "dropping the recovered lock should remove the lock file"
746 );
747 }
748
749 #[test]
750 fn acquire_lock_replaces_legacy_stale_lock() {
751 let temp = tempdir().expect("tempdir should exist");
752 let lock_path = temp.path().join(".install.lock");
753 std::fs::write(&lock_path, "").expect("legacy lock file should write");
754
755 let lock = acquire_lock(&lock_path, Duration::ZERO, Duration::ZERO)
756 .expect("legacy stale lock should recover");
757 let content = std::fs::read_to_string(&lock_path).expect("lock file should be readable");
758
759 assert!(
760 content.contains(&format!("pid={}\n", std::process::id())),
761 "lock file should contain current process owner: {content}"
762 );
763
764 drop(lock);
765 assert!(
766 !lock_path.exists(),
767 "dropping the recovered lock should remove the lock file"
768 );
769 }
770
771 #[cfg(unix)]
772 #[test]
773 fn acquire_lock_keeps_live_owner_lock() {
774 let temp = tempdir().expect("tempdir should exist");
775 let lock_path = temp.path().join(".install.lock");
776 std::fs::write(&lock_path, format!("pid={}\n", std::process::id()))
777 .expect("lock file should write");
778
779 let error = match acquire_lock(&lock_path, Duration::ZERO, Duration::ZERO) {
780 Ok(_) => panic!("live owner lock should not be replaced"),
781 Err(error) => error,
782 };
783
784 assert!(
785 error.to_string().contains("timed out"),
786 "unexpected error: {error}"
787 );
788 assert!(
789 lock_path.exists(),
790 "live owner lock should remain for its owner"
791 );
792 }
793
794 #[test]
795 fn managed_download_rejects_non_https_non_localhost_urls() {
796 let Some(target) = zcashd_target_triple() else {
797 return;
798 };
799 let temp = tempdir().expect("tempdir should exist");
800 let manifest = ZcashdReleaseManifest {
801 schema_version: 1,
802 release_tag: "test-release".to_string(),
803 artifacts: vec![ZcashdReleaseArtifact {
804 target_triple: target.to_string(),
805 runtime_archive_url: "http://example.com/zcashd.tar.gz".to_string(),
806 runtime_archive_sha256:
807 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
808 runtime_archive_member_binary_path: "./bin/zcashd".to_string(),
809 }],
810 };
811
812 let error = resolve_managed_zcashd_binary_from_manifest(&manifest, temp.path())
813 .expect_err("non-https URL should be rejected");
814 assert!(
815 error.to_string().contains("https"),
816 "unexpected error: {error}"
817 );
818 }
819
820 #[test]
821 fn managed_resolver_downloads_and_installs_from_http_archive() {
822 let Some(target) = zcashd_target_triple() else {
823 return;
824 };
825
826 let temp = tempdir().expect("tempdir should exist");
827 let archive_path = temp.path().join("zcashd-compat.tar.gz");
828 let binary_contents = b"#!/bin/sh\necho zcashd-compat-test\n";
829
830 {
831 let file =
832 std::fs::File::create(&archive_path).expect("archive file should be created");
833 let encoder = GzEncoder::new(file, Compression::default());
834 let mut tar = Builder::new(encoder);
835
836 let mut header = tar::Header::new_gnu();
837 header.set_path("./bin/zcashd").expect("path should be set");
838 header.set_size(binary_contents.len() as u64);
839 header.set_mode(0o755);
840 header.set_cksum();
841 tar.append(&header, &binary_contents[..])
842 .expect("archive member should be appended");
843 tar.finish().expect("archive should finish");
844 }
845
846 let archive_sha256 = sha256_hex_file(&archive_path).expect("archive hash should compute");
847 let archive_bytes = std::fs::read(&archive_path).expect("archive bytes should be readable");
848
849 let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind");
850 let address = listener
851 .local_addr()
852 .expect("listener should have local address");
853 let payload = archive_bytes.clone();
854 let server = thread::spawn(move || {
855 let (mut stream, _) = listener.accept().expect("one client should connect");
856 let mut request_buf = [0u8; 1024];
857 let _ = stream.read(&mut request_buf);
858 let response = format!(
859 "HTTP/1.1 200 OK\r\nContent-Type: application/gzip\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
860 payload.len()
861 );
862 stream
863 .write_all(response.as_bytes())
864 .expect("response headers should write");
865 stream
866 .write_all(&payload)
867 .expect("response body should write");
868 });
869
870 let local_http_url = format!("http://{address}/zcashd-compat.tar.gz");
871 let artifact = ZcashdReleaseArtifact {
872 target_triple: target.to_string(),
873 runtime_archive_url: local_http_url,
874 runtime_archive_sha256: archive_sha256,
875 runtime_archive_member_binary_path: "./bin/zcashd".to_string(),
876 };
877 let manifest = ZcashdReleaseManifest {
878 schema_version: 1,
879 release_tag: "test-release".to_string(),
880 artifacts: vec![artifact],
881 };
882 let resolved = resolve_managed_zcashd_binary_from_manifest(&manifest, temp.path())
883 .expect("managed resolver should install zcashd");
884 let installed = std::fs::read(&resolved).expect("installed zcashd should be readable");
885 assert_eq!(installed, binary_contents);
886
887 server.join().expect("server thread should exit cleanly");
888 }
889}