1use std::collections::HashMap;
38use std::path::{Path, PathBuf};
39
40pub mod brew_emulate;
41pub mod coverage;
42pub mod error;
43pub mod executor;
44pub mod formula;
45pub mod lockfile;
46pub mod manifest;
47pub mod package_index;
48pub mod prebuilt;
49pub mod recipe;
50pub mod recipe_choco;
51pub mod registry;
52pub mod relocate;
53pub mod source_build;
54pub mod windows;
55
56pub use error::{Result, ToolchainError};
57pub use lockfile::{LockedTool, ToolchainLockfile, ToolchainLockfileExt, LOCKFILE_NAME};
58
59use crate::coverage::{CoverageRecord, CoverageStatus};
60use crate::registry::{PublishedToolchain, PulledToolchain, ToolchainArtifactId};
61use crate::relocate::RelocationReport;
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum ToolPlatform {
75 MacOS,
77 Windows,
79}
80
81#[derive(Debug, Clone)]
88pub struct ToolchainHandle {
89 pub install_dir: PathBuf,
91 pub path_dirs: Vec<String>,
93 pub env: HashMap<String, String>,
95}
96
97fn arch_token() -> &'static str {
99 match std::env::consts::ARCH {
100 "aarch64" => "arm64",
101 other => other,
102 }
103}
104
105fn split_pkg(pkg: &str) -> (&str, &str) {
111 match pkg.split_once('@') {
112 Some((_, ver)) if !ver.is_empty() => (pkg, ver),
113 _ => (pkg, "latest"),
114 }
115}
116
117pub async fn ensure_toolchain(
136 pkg: &str,
137 platform: ToolPlatform,
138 cache_dir: &Path,
139 lockfile: Option<&ToolchainLockfile>,
140) -> Result<ToolchainHandle> {
141 match platform {
142 ToolPlatform::Windows => {
143 let toolchain = windows::ensure_windows_toolchain(pkg, cache_dir, lockfile).await?;
144 build_handle_from_toolchain(toolchain).await
145 }
146 ToolPlatform::MacOS => {
147 let toolchain = ensure_macos_toolchain(pkg, cache_dir, lockfile).await?;
148 build_handle_from_toolchain(toolchain).await
149 }
150 }
151}
152
153pub(crate) async fn ensure_macos_toolchain(
161 pkg: &str,
162 cache_dir: &Path,
163 lockfile: Option<&ToolchainLockfile>,
164) -> Result<PathBuf> {
165 let (formula, _version) = split_pkg(pkg);
166 if prebuilt::is_prebuilt_formula(formula) {
167 prebuilt::ensure_prebuilt(formula, cache_dir, lockfile).await
170 } else {
171 source_build::ensure_from_source(formula, cache_dir, lockfile).await
174 }
175}
176
177fn vendor_arch(arch: &str) -> &str {
180 if arch == "x86_64" {
181 "amd64"
182 } else {
183 arch
184 }
185}
186
187fn platform_token(platform: ToolPlatform) -> &'static str {
189 match platform {
190 ToolPlatform::MacOS => "macos",
191 ToolPlatform::Windows => "windows",
192 }
193}
194
195fn sanitize(tool: &str) -> String {
197 tool.chars()
198 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
199 .collect()
200}
201
202pub async fn resolve_locked_tool(
214 tool: &str,
215 platform: ToolPlatform,
216 arch: &str,
217) -> Result<LockedTool> {
218 let (formula, _version) = split_pkg(tool);
219 let (version, url, expected): (String, String, Option<String>) = match platform {
220 ToolPlatform::MacOS => {
221 if prebuilt::is_prebuilt_formula(formula) {
222 let r = prebuilt::resolve_prebuilt(formula, vendor_arch(arch)).await?;
223 (r.version, r.url, r.sha256)
224 } else {
225 let spec = source_build::resolve_source_spec(formula).await?;
226 let sha = (!spec.sha256.is_empty()).then_some(spec.sha256);
227 (spec.version, spec.tarball_url, sha)
228 }
229 }
230 ToolPlatform::Windows => windows::resolve_locked_windows(formula).await?,
231 };
232
233 let tmp = std::env::temp_dir().join(format!(
235 "zlayer-lock-{}-{arch}-{}",
236 sanitize(tool),
237 std::process::id()
238 ));
239 let sha256 = package_index::download_verified(&url, &tmp, expected.as_deref()).await?;
240 let _ = tokio::fs::remove_file(&tmp).await;
241
242 Ok(LockedTool {
243 tool: tool.to_string(),
244 platform: platform_token(platform).to_string(),
245 arch: arch.to_string(),
246 version,
247 url,
248 sha256,
249 resolved_at: chrono::Utc::now().to_rfc3339(),
250 })
251}
252
253async fn build_handle_from_toolchain(toolchain: PathBuf) -> Result<ToolchainHandle> {
256 let manifest = manifest::ToolchainManifest::load_or_synthesize(&toolchain).await?;
257 Ok(ToolchainHandle {
258 install_dir: toolchain,
259 path_dirs: manifest.path_dirs,
260 env: manifest.env,
261 })
262}
263
264pub async fn probe_ready_toolchain(
276 pkg: &str,
277 _platform: ToolPlatform,
278 cache_dir: &Path,
279) -> Option<ToolchainHandle> {
280 let (formula, _version) = split_pkg(pkg);
283 let toolchain = newest_ready_toolchain(formula, cache_dir).await?;
284 build_handle_from_toolchain(toolchain).await.ok()
285}
286
287async fn newest_ready_toolchain(formula: &str, cache_dir: &Path) -> Option<PathBuf> {
290 let prefix = format!("{formula}-");
291 let arch_suffix = format!("-{}", arch_token());
292 let mut entries = tokio::fs::read_dir(cache_dir).await.ok()?;
293 let mut best: Option<(std::time::SystemTime, PathBuf)> = None;
294 while let Ok(Some(entry)) = entries.next_entry().await {
295 let name = entry.file_name();
296 let Some(name) = name.to_str() else { continue };
297 if !name.starts_with(&prefix) || !name.ends_with(&arch_suffix) {
298 continue;
299 }
300 let toolchain = entry.path();
301 if !tokio::fs::try_exists(toolchain.join(".ready"))
302 .await
303 .unwrap_or(false)
304 {
305 continue;
306 }
307 let mtime = entry
308 .metadata()
309 .await
310 .ok()
311 .and_then(|m| m.modified().ok())
312 .unwrap_or(std::time::UNIX_EPOCH);
313 if best.as_ref().is_none_or(|(t, _)| mtime >= *t) {
314 best = Some((mtime, toolchain));
315 }
316 }
317 best.map(|(_, toolchain)| toolchain)
318}
319
320const ERROR_TAIL_MAX: usize = 2048;
335
336pub(crate) async fn pull_first(id: &ToolchainArtifactId, toolchain: &Path) -> Result<Option<()>> {
350 match registry::try_pull_toolchain(id, toolchain).await {
351 Ok(Some(pulled)) => {
352 stamp_ready(toolchain).await?;
353 record_pull_built(id, &pulled).await;
354 Ok(Some(()))
355 }
356 Ok(None) => Ok(None),
357 Err(e) => {
358 tracing::warn!(
359 tool = %id.tool,
360 error = %e,
361 "toolchain registry pull failed (non-fatal); building instead"
362 );
363 Ok(None)
364 }
365 }
366}
367
368pub(crate) async fn stamp_ready(toolchain: &Path) -> Result<()> {
377 tokio::fs::write(toolchain.join(".ready"), b"").await?;
378 Ok(())
379}
380
381pub(crate) async fn finish_built(
392 id: &ToolchainArtifactId,
393 toolchain: &Path,
394 report: Option<&RelocationReport>,
395 is_prebuilt: bool,
396) {
397 let published = if should_publish(is_prebuilt) {
398 match report {
399 Some(report) => try_publish(id, toolchain, report).await,
400 None => None,
401 }
402 } else {
403 None
404 };
405 coverage::record(&build_coverage(
406 id,
407 CoverageStatus::Built,
408 published.as_ref(),
409 None,
410 ))
411 .await;
412}
413
414pub(crate) async fn record_net_fallback(id: &ToolchainArtifactId) {
418 coverage::record(&build_coverage(id, CoverageStatus::NetFallback, None, None)).await;
419}
420
421pub(crate) async fn record_failed(id: &ToolchainArtifactId, err: &ToolchainError) {
424 let tail = err.to_string();
425 coverage::record(&build_coverage(
426 id,
427 CoverageStatus::Failed,
428 None,
429 Some(&tail),
430 ))
431 .await;
432}
433
434async fn record_pull_built(id: &ToolchainArtifactId, pulled: &PulledToolchain) {
438 let published = PublishedToolchain {
439 reference: pulled.reference.clone(),
440 digest: pulled.digest.clone(),
441 };
442 coverage::record(&build_coverage(
443 id,
444 CoverageStatus::Built,
445 Some(&published),
446 None,
447 ))
448 .await;
449}
450
451async fn try_publish(
457 id: &ToolchainArtifactId,
458 toolchain: &Path,
459 report: &RelocationReport,
460) -> Option<PublishedToolchain> {
461 let manifest = match manifest::ToolchainManifest::read_from_toolchain(toolchain).await {
462 Ok(Some(manifest)) => manifest,
463 Ok(None) => {
464 tracing::warn!(tool = %id.tool, "no manifest present to publish; skipping publish");
465 return None;
466 }
467 Err(e) => {
468 tracing::warn!(tool = %id.tool, error = %e, "reading manifest for publish failed; skipping publish");
469 return None;
470 }
471 };
472 match registry::publish_toolchain(toolchain, &manifest, report, toolchain, id).await {
473 Ok(published) => Some(published),
474 Err(e) => {
475 tracing::warn!(
476 tool = %id.tool,
477 error = %e,
478 "toolchain publish failed (non-fatal; an anonymous / no-token push is expected)"
479 );
480 None
481 }
482 }
483}
484
485fn should_publish(is_prebuilt: bool) -> bool {
492 !is_prebuilt
493}
494
495fn build_coverage(
502 id: &ToolchainArtifactId,
503 status: CoverageStatus,
504 published: Option<&PublishedToolchain>,
505 err: Option<&str>,
506) -> CoverageRecord {
507 CoverageRecord {
508 tool: id.tool.clone(),
509 platform: id.os.clone(),
510 arch: id.arch.clone(),
511 status,
512 version: id.version.clone(),
513 registry_ref: published.map(|p| p.reference.clone()).unwrap_or_default(),
514 registry_digest: published.map(|p| p.digest.clone()).unwrap_or_default(),
515 error_tail: err.map(truncate_error_tail).unwrap_or_default(),
516 recorded_at: coverage::now_rfc3339(),
517 }
518}
519
520fn truncate_error_tail(err: &str) -> String {
523 if err.len() <= ERROR_TAIL_MAX {
524 return err.to_string();
525 }
526 let mut end = ERROR_TAIL_MAX;
527 while end > 0 && !err.is_char_boundary(end) {
528 end -= 1;
529 }
530 err[..end].to_string()
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536 use crate::manifest::{ToolchainManifest, ToolchainSource};
537
538 #[test]
539 fn split_pkg_plain_defaults_to_latest() {
540 assert_eq!(split_pkg("git"), ("git", "latest"));
541 }
542
543 #[test]
544 fn split_pkg_versioned_keeps_full_formula() {
545 assert_eq!(split_pkg("openssl@3"), ("openssl@3", "3"));
546 }
547
548 #[test]
549 fn split_pkg_trailing_at_is_latest() {
550 assert_eq!(split_pkg("weird@"), ("weird@", "latest"));
551 }
552
553 fn sample_id() -> ToolchainArtifactId {
556 ToolchainArtifactId {
557 tool: "jq".to_string(),
558 version: "1.8.1".to_string(),
559 os: "macos".to_string(),
560 arch: "arm64".to_string(),
561 }
562 }
563
564 #[test]
566 fn build_coverage_built_carries_registry_ref_and_digest() {
567 let id = sample_id();
568 let published = PublishedToolchain {
569 reference: "ghcr.io/blackleafdigital/zlayer/toolchains:jq-1.8.1-macos-arm64"
570 .to_string(),
571 digest: "sha256:abc".to_string(),
572 };
573 let rec = build_coverage(&id, CoverageStatus::Built, Some(&published), None);
574 assert_eq!(rec.tool, "jq");
575 assert_eq!(rec.platform, "macos");
576 assert_eq!(rec.arch, "arm64");
577 assert_eq!(rec.version, "1.8.1");
578 assert_eq!(rec.status, CoverageStatus::Built);
579 assert_eq!(rec.registry_ref, published.reference);
580 assert_eq!(rec.registry_digest, "sha256:abc");
581 assert!(rec.error_tail.is_empty());
582 assert!(!rec.recorded_at.is_empty(), "recorded_at is stamped");
583 }
584
585 #[test]
588 fn build_coverage_built_without_publish_has_no_ref() {
589 let rec = build_coverage(&sample_id(), CoverageStatus::Built, None, None);
590 assert_eq!(rec.status, CoverageStatus::Built);
591 assert!(rec.registry_ref.is_empty());
592 assert!(rec.registry_digest.is_empty());
593 assert!(rec.error_tail.is_empty());
594 }
595
596 #[test]
598 fn build_coverage_failed_carries_error_tail() {
599 let err = "Registry error: generic source build blew up".to_string();
600 let rec = build_coverage(&sample_id(), CoverageStatus::Failed, None, Some(&err));
601 assert_eq!(rec.status, CoverageStatus::Failed);
602 assert_eq!(rec.error_tail, err);
603 assert!(rec.registry_ref.is_empty());
604 assert!(rec.registry_digest.is_empty());
605 }
606
607 #[test]
609 fn build_coverage_net_fallback_status() {
610 let rec = build_coverage(&sample_id(), CoverageStatus::NetFallback, None, None);
611 assert_eq!(rec.status, CoverageStatus::NetFallback);
612 assert!(rec.registry_ref.is_empty());
613 assert!(rec.error_tail.is_empty());
614 }
615
616 #[test]
618 fn error_tail_truncates_to_cap_on_char_boundary() {
619 let short = "small error";
620 assert_eq!(truncate_error_tail(short), short);
621
622 let long = "é".repeat(4000);
624 let cut = truncate_error_tail(&long);
625 assert!(cut.len() <= ERROR_TAIL_MAX);
626 assert!(cut.len() > ERROR_TAIL_MAX - 4, "cut near the cap, not tiny");
627 assert!(
628 long.starts_with(&cut),
629 "a valid-UTF-8 prefix of the original"
630 );
631 }
632
633 #[test]
636 fn should_publish_excludes_prebuilt() {
637 assert!(
638 should_publish(false),
639 "source-build / Windows toolchains publish"
640 );
641 assert!(
642 !should_publish(true),
643 "language prebuilts are never published"
644 );
645 }
646
647 #[tokio::test]
656 async fn stamp_ready_writes_marker_without_clobbering_registry_manifest() {
657 let tmp = tempfile::tempdir().unwrap();
658 let dir = tmp.path();
659 let mut env = HashMap::new();
660 env.insert("K".to_string(), format!("{}/lib", dir.display()));
661 let manifest = ToolchainManifest {
662 tool: "jq".to_string(),
663 version: "1.8.1".to_string(),
664 arch: "arm64".to_string(),
665 platform: "macos".to_string(),
666 path_dirs: vec![format!("{}/bin", dir.display())],
667 env,
668 source: ToolchainSource::Registry {
669 reference: "ghcr.io/x/toolchains:jq-1.8.1-macos-arm64".to_string(),
670 digest: "sha256:deadbeef".to_string(),
671 },
672 build_deps: vec![],
673 provisioned_at: "2026-07-06T00:00:00Z".to_string(),
674 };
675 manifest.write_to_toolchain(dir).await.unwrap();
676 assert!(
677 !dir.join(".ready").exists(),
678 "pull leaves .ready to the caller"
679 );
680
681 stamp_ready(dir).await.unwrap();
682
683 assert!(dir.join(".ready").is_file(), ".ready stamped");
684 let reread = ToolchainManifest::read_from_toolchain(dir)
685 .await
686 .unwrap()
687 .unwrap();
688 match reread.source {
689 ToolchainSource::Registry { reference, digest } => {
690 assert_eq!(reference, "ghcr.io/x/toolchains:jq-1.8.1-macos-arm64");
691 assert_eq!(digest, "sha256:deadbeef");
692 }
693 other => panic!("stamp_ready must NOT rewrite the Registry manifest; got {other:?}"),
694 }
695 }
696
697 #[tokio::test]
704 #[ignore = "live: resolves + downloads a Chocolatey package through the package index"]
705 async fn windows_exe_installer_without_executor_is_executor_unavailable() {
706 let tmp = tempfile::tempdir().unwrap();
707 let err = ensure_toolchain("7zip", ToolPlatform::Windows, tmp.path(), None)
708 .await
709 .unwrap_err();
710 assert!(
711 matches!(err, ToolchainError::ExecutorUnavailable { .. }),
712 "{err}"
713 );
714 }
715
716 async fn seed_legacy_git_toolchain(cache_dir: &Path, version: &str) -> PathBuf {
720 let toolchain = cache_dir.join(format!("git-{version}-{}", arch_token()));
721 tokio::fs::create_dir_all(toolchain.join("bin"))
722 .await
723 .unwrap();
724 tokio::fs::create_dir_all(toolchain.join("libexec/git-core"))
725 .await
726 .unwrap();
727 tokio::fs::create_dir_all(toolchain.join("etc"))
728 .await
729 .unwrap();
730 tokio::fs::write(toolchain.join("etc/gitconfig"), b"")
731 .await
732 .unwrap();
733 tokio::fs::write(toolchain.join(".ready"), b"")
734 .await
735 .unwrap();
736 toolchain
737 }
738
739 async fn seed_toolchain_with_manifest(cache_dir: &Path, tool: &str, version: &str) -> PathBuf {
742 let toolchain = cache_dir.join(format!("{tool}-{version}-{}", arch_token()));
743 let bin = toolchain.join("bin");
744 tokio::fs::create_dir_all(&bin).await.unwrap();
745 let mut env = HashMap::new();
746 env.insert("FOO".to_string(), "bar".to_string());
747 let manifest = ToolchainManifest {
748 tool: tool.to_string(),
749 version: version.to_string(),
750 arch: arch_token().to_string(),
751 platform: "macos".to_string(),
752 path_dirs: vec![bin.display().to_string()],
753 env,
754 source: ToolchainSource::SourceBuild {
755 url: String::new(),
756 sha256: String::new(),
757 },
758 build_deps: vec![],
759 provisioned_at: "2026-06-30T00:00:00Z".to_string(),
760 };
761 manifest.write_to_toolchain(&toolchain).await.unwrap();
762 tokio::fs::write(toolchain.join(".ready"), b"")
763 .await
764 .unwrap();
765 toolchain
766 }
767
768 #[tokio::test]
772 async fn handle_synthesized_for_legacy_git_toolchain_drops_dyld() {
773 let tmp = tempfile::tempdir().unwrap();
774 let toolchain = seed_legacy_git_toolchain(tmp.path(), "2.55.0").await;
775
776 let handle = build_handle_from_toolchain(toolchain.clone())
777 .await
778 .unwrap();
779
780 assert_eq!(handle.install_dir, toolchain);
781 assert_eq!(
782 handle.path_dirs,
783 vec![toolchain.join("bin").display().to_string()]
784 );
785 assert_eq!(
786 handle.env.get("GIT_EXEC_PATH"),
787 Some(&toolchain.join("libexec/git-core").display().to_string())
788 );
789 assert!(!handle.env.contains_key("DYLD_FALLBACK_LIBRARY_PATH"));
790 assert!(!handle.env.contains_key("GIT_CONFIG_SYSTEM"));
791 }
792
793 #[tokio::test]
796 async fn handle_reads_manifest_when_present() {
797 let tmp = tempfile::tempdir().unwrap();
798 let toolchain = seed_toolchain_with_manifest(tmp.path(), "jq", "1.8.2").await;
799
800 let handle = build_handle_from_toolchain(toolchain.clone())
801 .await
802 .unwrap();
803 assert_eq!(handle.install_dir, toolchain);
804 assert_eq!(
805 handle.path_dirs,
806 vec![toolchain.join("bin").display().to_string()]
807 );
808 assert_eq!(handle.env.get("FOO"), Some(&"bar".to_string()));
809 }
810
811 #[tokio::test]
814 async fn probe_ready_returns_handle_for_ready_toolchain() {
815 let tmp = tempfile::tempdir().unwrap();
816 let toolchain = seed_legacy_git_toolchain(tmp.path(), "2.55.0").await;
817
818 let handle = probe_ready_toolchain("git", ToolPlatform::MacOS, tmp.path())
819 .await
820 .expect("ready toolchain should be probed without install");
821
822 assert_eq!(handle.install_dir, toolchain);
823 assert_eq!(
824 handle.env.get("GIT_EXEC_PATH"),
825 Some(&toolchain.join("libexec/git-core").display().to_string())
826 );
827 }
828
829 #[tokio::test]
831 async fn probe_ready_is_generic_across_tools() {
832 let tmp = tempfile::tempdir().unwrap();
833 let toolchain = seed_toolchain_with_manifest(tmp.path(), "jq", "1.8.2").await;
834
835 let handle = probe_ready_toolchain("jq", ToolPlatform::MacOS, tmp.path())
836 .await
837 .expect("ready jq toolchain should be probed");
838 assert_eq!(handle.install_dir, toolchain);
839 assert_eq!(handle.env.get("FOO"), Some(&"bar".to_string()));
840 }
841
842 #[tokio::test]
845 async fn probe_ready_returns_none_without_ready_marker() {
846 let tmp = tempfile::tempdir().unwrap();
847 let toolchain = tmp.path().join(format!("git-2.55.0-{}", arch_token()));
848 tokio::fs::create_dir_all(toolchain.join("bin"))
849 .await
850 .unwrap();
851
852 assert!(
853 probe_ready_toolchain("git", ToolPlatform::MacOS, tmp.path())
854 .await
855 .is_none(),
856 "an unstamped toolchain must not be injected"
857 );
858 }
859
860 #[tokio::test]
862 async fn probe_ready_returns_none_for_cold_or_unsupported() {
863 let tmp = tempfile::tempdir().unwrap();
864 assert!(
865 probe_ready_toolchain("git", ToolPlatform::MacOS, tmp.path())
866 .await
867 .is_none(),
868 "cold cache should probe None"
869 );
870 assert!(
871 probe_ready_toolchain("jq", ToolPlatform::MacOS, tmp.path())
872 .await
873 .is_none(),
874 "absent tool should probe None"
875 );
876 assert!(
877 probe_ready_toolchain("git", ToolPlatform::Windows, tmp.path())
878 .await
879 .is_none(),
880 "cold cache probes None on every platform"
881 );
882 }
883
884 #[tokio::test]
891 #[ignore = "live build test; fetches + compiles git and jq from source (macOS + CLT only)"]
892 async fn ensure_git_and_jq_build_from_source_and_run() {
893 let tmp = tempfile::tempdir().unwrap();
894
895 for (tool, version_needle) in [("git", "git version"), ("jq", "jq-")] {
896 let handle = ensure_toolchain(tool, ToolPlatform::MacOS, tmp.path(), None)
897 .await
898 .unwrap_or_else(|e| panic!("{tool} toolchain should build from source: {e}"));
899
900 let bin = handle.install_dir.join("bin").join(tool);
901 assert!(
902 bin.exists(),
903 "{tool} binary should exist at <toolchain>/bin/{tool}"
904 );
905
906 let manifest = ToolchainManifest::read_from_toolchain(&handle.install_dir)
908 .await
909 .unwrap()
910 .unwrap_or_else(|| panic!("{tool} toolchain must have a manifest"));
911 assert_eq!(manifest.tool, tool);
912
913 let bytes = tokio::fs::read(&bin).await.expect("read binary");
915 assert!(
916 !contains_subslice_bytes(&bytes, b"@@HOMEBREW"),
917 "source-built {tool} must contain NO @@HOMEBREW@@ references"
918 );
919
920 let mut cmd = tokio::process::Command::new(&bin);
921 for (k, v) in &handle.env {
922 cmd.env(k, v);
923 }
924 let out = cmd.arg("--version").output().await.expect("run --version");
925 assert!(out.status.success(), "{tool} --version should succeed");
926 assert!(
927 String::from_utf8_lossy(&out.stdout).contains(version_needle)
928 || String::from_utf8_lossy(&out.stderr).contains(version_needle),
929 "{tool} --version output should contain '{version_needle}'"
930 );
931 }
932 }
933
934 fn contains_subslice_bytes(haystack: &[u8], needle: &[u8]) -> bool {
936 if needle.is_empty() || haystack.len() < needle.len() {
937 return false;
938 }
939 haystack
940 .windows(needle.len())
941 .any(|window| window == needle)
942 }
943}