forc_pkg/source/
mod.rs

1//! Related to pinning, fetching, validating and caching the source for packages.
2//!
3//! To add a new source kind:
4//!
5//! 1. Add a new module.
6//! 2. Create types providing implementations for each of the traits in this module.
7//! 3. Add a variant to the `Source` and `Pinned` types in this module.
8//! 4. Add variant support to the `from_manifest_dep` and `FromStr` implementations.
9
10pub mod git;
11pub(crate) mod ipfs;
12mod member;
13pub mod path;
14pub mod reg;
15
16use self::git::Url;
17use crate::manifest::GenericManifestFile;
18use crate::{
19    manifest::{self, MemberManifestFiles, PackageManifestFile},
20    pkg::{ManifestMap, PinnedId},
21};
22use anyhow::{anyhow, bail, Result};
23use serde::{Deserialize, Serialize};
24use std::{
25    collections::hash_map,
26    fmt,
27    hash::{Hash, Hasher},
28    path::{Path, PathBuf},
29    str::FromStr,
30};
31use sway_utils::{DEFAULT_IPFS_GATEWAY_URL, DEFAULT_REGISTRY_IPFS_GATEWAY_URL};
32
33/// Pin this source at a specific "version", return the local directory to fetch into.
34trait Pin {
35    type Pinned: Fetch + Hash;
36    fn pin(&self, ctx: PinCtx) -> Result<(Self::Pinned, PathBuf)>;
37}
38
39/// Fetch (and optionally cache) a pinned instance of this source to the given path.
40trait Fetch {
41    fn fetch(&self, ctx: PinCtx, local: &Path) -> Result<PackageManifestFile>;
42}
43
44/// Given a parent manifest, return the canonical, local path for this source as a dependency.
45trait DepPath {
46    fn dep_path(&self, name: &str) -> Result<DependencyPath>;
47}
48
49type FetchId = u64;
50
51#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
52pub enum IPFSNode {
53    Local,
54    WithUrl(String),
55}
56
57impl Default for IPFSNode {
58    fn default() -> Self {
59        Self::WithUrl(DEFAULT_IPFS_GATEWAY_URL.to_string())
60    }
61}
62
63impl IPFSNode {
64    /// Returns an IPFSNode configured to use the Fuel-operated IPFS gateway.
65    pub fn fuel() -> Self {
66        Self::WithUrl(DEFAULT_REGISTRY_IPFS_GATEWAY_URL.to_string())
67    }
68
69    /// Returns an IPFSNode configured to use the public IPFS gateway.
70    pub fn public() -> Self {
71        Self::WithUrl(DEFAULT_IPFS_GATEWAY_URL.to_string())
72    }
73}
74
75impl FromStr for IPFSNode {
76    type Err = anyhow::Error;
77
78    fn from_str(value: &str) -> Result<Self, Self::Err> {
79        match value {
80            "PUBLIC" => {
81                let url = sway_utils::constants::DEFAULT_IPFS_GATEWAY_URL;
82                Ok(IPFSNode::WithUrl(url.to_string()))
83            }
84            "FUEL" => {
85                let url = sway_utils::constants::DEFAULT_REGISTRY_IPFS_GATEWAY_URL;
86                Ok(IPFSNode::WithUrl(url.to_string()))
87            }
88            "LOCAL" => Ok(IPFSNode::Local),
89            url => Ok(IPFSNode::WithUrl(url.to_string())),
90        }
91    }
92}
93
94/// Specifies a base source for a package.
95///
96/// - For registry packages, this includes a base version.
97/// - For git packages, this includes a base git reference like a branch or tag.
98///
99/// Note that a `Source` does not specify a specific, pinned version. Rather, it specifies a source
100/// at which the current latest version may be located.
101#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
102pub enum Source {
103    /// Used to refer to a workspace member project.
104    Member(member::Source),
105    /// A git repo with a `Forc.toml` manifest at its root.
106    Git(git::Source),
107    /// A path to a directory with a `Forc.toml` manifest at its root.
108    Path(path::Source),
109    /// A package described by its IPFS CID.
110    Ipfs(ipfs::Source),
111    /// A forc project hosted on the official registry.
112    Registry(reg::Source),
113}
114
115/// A pinned instance of the package source.
116///
117/// Specifies an exact version to use, or an exact commit in the case of git dependencies. The
118/// pinned version or commit is updated upon creation of the lock file and on `forc update`.
119#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
120pub enum Pinned {
121    Member(member::Pinned),
122    Git(git::Pinned),
123    Path(path::Pinned),
124    Ipfs(ipfs::Pinned),
125    Registry(reg::Pinned),
126}
127
128#[derive(Clone)]
129pub(crate) struct PinCtx<'a> {
130    /// A unique hash associated with the process' current fetch pass.
131    /// NOTE: Only to be used for creating temporary directories. Should not
132    /// interact with anything that appears in the pinned output.
133    pub(crate) fetch_id: FetchId,
134    /// Within the context of a package graph fetch traversal, represents the current path root.
135    pub(crate) path_root: PinnedId,
136    /// Whether or not the fetch is occurring offline.
137    pub(crate) offline: bool,
138    /// The name of the package associated with this source.
139    pub(crate) name: &'a str,
140    /// The IPFS node to use for fetching IPFS sources.
141    pub(crate) ipfs_node: &'a IPFSNode,
142}
143
144pub(crate) enum DependencyPath {
145    /// The dependency is another member of the workspace.
146    Member,
147    /// The dependency is located at this specific path.
148    ManifestPath(PathBuf),
149    /// Path is pinned via manifest, relative to the given root node.
150    Root(PinnedId),
151}
152
153/// A wrapper type for providing `Display` implementations for compiling msgs.
154pub struct DisplayCompiling<'a, T> {
155    source: &'a T,
156    manifest_dir: &'a Path,
157}
158
159/// Error returned upon failed parsing of `SourcePinned::from_str`.
160#[derive(Clone, Debug)]
161pub struct PinnedParseError;
162
163impl Source {
164    /// Construct a source from path information collected from manifest file.
165    fn with_path_dependency(
166        relative_path: &Path,
167        manifest_dir: &Path,
168        member_manifests: &MemberManifestFiles,
169    ) -> Result<Self> {
170        let path = manifest_dir.join(relative_path);
171        let canonical_path = path
172            .canonicalize()
173            .map_err(|e| anyhow!("Failed to canonicalize dependency path {:?}: {}", path, e))?;
174        // Check if path is a member of a workspace.
175        if member_manifests
176            .values()
177            .any(|pkg_manifest| pkg_manifest.dir() == canonical_path)
178        {
179            Ok(Source::Member(member::Source(canonical_path)))
180        } else {
181            Ok(Source::Path(canonical_path))
182        }
183    }
184
185    /// Construct a source from version information collected from manifest file.
186    fn with_version_dependency(
187        pkg_name: &str,
188        version: &str,
189        namespace: &reg::file_location::Namespace,
190    ) -> Result<Self> {
191        // TODO: update here once we are supporting non-exact versions (non `x.y.z` versions)
192        // see: https://github.com/FuelLabs/sway/issues/7060
193        let semver = semver::Version::parse(version)?;
194        let source = reg::Source {
195            version: semver,
196            namespace: namespace.clone(),
197            name: pkg_name.to_string(),
198        };
199        Ok(Source::Registry(source))
200    }
201
202    /// Convert the given manifest `Dependency` declaration to a `Source`.
203    pub fn from_manifest_dep(
204        manifest_dir: &Path,
205        dep_name: &str,
206        dep: &manifest::Dependency,
207        member_manifests: &MemberManifestFiles,
208    ) -> Result<Self> {
209        let source = match dep {
210            manifest::Dependency::Simple(ref ver_str) => Source::with_version_dependency(
211                dep_name,
212                ver_str,
213                &reg::file_location::Namespace::Flat,
214            )?,
215            manifest::Dependency::Detailed(ref det) => {
216                match (&det.path, &det.version, &det.git, &det.ipfs) {
217                    (Some(relative_path), _, _, _) => {
218                        let relative_path = PathBuf::from_str(relative_path)?;
219                        Source::with_path_dependency(
220                            &relative_path,
221                            manifest_dir,
222                            member_manifests,
223                        )?
224                    }
225                    (_, _, Some(repo), _) => {
226                        let reference = match (&det.branch, &det.tag, &det.rev) {
227                            (Some(branch), None, None) => git::Reference::Branch(branch.clone()),
228                            (None, Some(tag), None) => git::Reference::Tag(tag.clone()),
229                            (None, None, Some(rev)) => git::Reference::Rev(rev.clone()),
230                            (None, None, None) => git::Reference::DefaultBranch,
231                            _ => bail!(
232                                "git dependencies support at most one reference: \
233                                either `branch`, `tag` or `rev`"
234                            ),
235                        };
236                        let repo = Url::from_str(repo)?;
237                        let source = git::Source { repo, reference };
238                        Source::Git(source)
239                    }
240                    (_, _, _, Some(ipfs)) => {
241                        let cid = ipfs.parse()?;
242                        let source = ipfs::Source(cid);
243                        Source::Ipfs(source)
244                    }
245                    (None, Some(version), _, _) => {
246                        let namespace = det.namespace.as_ref().map_or_else(
247                            || reg::file_location::Namespace::Flat,
248                            |ns| reg::file_location::Namespace::Domain(ns.to_string()),
249                        );
250                        Source::with_version_dependency(dep_name, version, &namespace)?
251                    }
252                    _ => {
253                        bail!("unsupported set of fields for dependency: {:?}", dep);
254                    }
255                }
256            }
257        };
258        Ok(source)
259    }
260
261    /// Convert the given manifest `Dependency` declaration to a source,
262    /// applying any relevant patches from within the given `manifest` as
263    /// necessary.
264    pub fn from_manifest_dep_patched(
265        manifest: &PackageManifestFile,
266        dep_name: &str,
267        dep: &manifest::Dependency,
268        members: &MemberManifestFiles,
269    ) -> Result<Self> {
270        let unpatched = Self::from_manifest_dep(manifest.dir(), dep_name, dep, members)?;
271        unpatched.apply_patch(dep_name, manifest, members)
272    }
273
274    /// If a patch exists for this dependency source within the given project
275    /// manifest, this returns the patch.
276    ///
277    /// Supports patching both Git and Registry dependencies:
278    /// - Git: [patch.'https://github.com/org/repo']
279    /// - Registry: [patch.'forc.pub']
280    ///
281    /// Note: Quotes are required around patch keys containing dots to follow TOML spec.
282    /// Without quotes, `[patch.forc.pub]` creates nested tables instead of a single key.
283    fn dep_patch(
284        &self,
285        dep_name: &str,
286        manifest: &PackageManifestFile,
287    ) -> Result<Option<manifest::Dependency>> {
288        // Helper to check if a patch exists for the given key
289        let check_patches = |patch_key: &str| -> Result<Option<manifest::Dependency>> {
290            let patches = manifest.resolve_patch(patch_key)?;
291            Ok(patches.and_then(|p| p.get(dep_name).cloned()))
292        };
293
294        match self {
295            Source::Git(git) => {
296                let git_url = git.repo.to_string();
297                check_patches(&git_url)
298            }
299            Source::Registry(reg_source) => {
300                // Try namespace-specific patch first (more specific takes priority)
301                if let reg::file_location::Namespace::Domain(ns) = &reg_source.namespace {
302                    let namespaced_key = format!("{}/{}", reg::REGISTRY_PATCH_KEY, ns);
303                    if let Some(patch) = check_patches(&namespaced_key)? {
304                        return Ok(Some(patch));
305                    }
306                }
307
308                // Fall back to generic registry patch
309                check_patches(reg::REGISTRY_PATCH_KEY)
310            }
311            _ => Ok(None),
312        }
313    }
314
315    /// If a patch exists for the dependency associated with this source within
316    /// the given manifest, this returns a new `Source` with the patch applied.
317    ///
318    /// If no patch exists, this returns the original `Source`.
319    pub fn apply_patch(
320        &self,
321        dep_name: &str,
322        manifest: &PackageManifestFile,
323        members: &MemberManifestFiles,
324    ) -> Result<Self> {
325        match self.dep_patch(dep_name, manifest)? {
326            Some(patch) => Self::from_manifest_dep(manifest.dir(), dep_name, &patch, members),
327            None => Ok(self.clone()),
328        }
329    }
330
331    /// Attempt to determine the pinned version or commit for the source.
332    ///
333    /// Also updates the manifest map with a path to the local copy of the pkg.
334    ///
335    /// The `path_root` is required for `Path` dependencies and must specify the package that is the
336    /// root of the current subgraph of path dependencies.
337    pub(crate) fn pin(&self, ctx: PinCtx, manifests: &mut ManifestMap) -> Result<Pinned> {
338        fn f<T>(source: &T, ctx: PinCtx, manifests: &mut ManifestMap) -> Result<T::Pinned>
339        where
340            T: Pin,
341            T::Pinned: Clone,
342            Pinned: From<T::Pinned>,
343        {
344            let (pinned, fetch_path) = source.pin(ctx.clone())?;
345            let id = PinnedId::new(ctx.name(), &Pinned::from(pinned.clone()));
346            if let hash_map::Entry::Vacant(entry) = manifests.entry(id) {
347                entry.insert(pinned.fetch(ctx, &fetch_path)?);
348            }
349            Ok(pinned)
350        }
351        match self {
352            Source::Member(source) => Ok(Pinned::Member(f(source, ctx, manifests)?)),
353            Source::Path(source) => Ok(Pinned::Path(f(source, ctx, manifests)?)),
354            Source::Git(source) => Ok(Pinned::Git(f(source, ctx, manifests)?)),
355            Source::Ipfs(source) => Ok(Pinned::Ipfs(f(source, ctx, manifests)?)),
356            Source::Registry(source) => Ok(Pinned::Registry(f(source, ctx, manifests)?)),
357        }
358    }
359}
360
361impl Pinned {
362    pub(crate) const MEMBER: Self = Self::Member(member::Pinned);
363
364    /// Return how the pinned source for a dependency can be found on the local file system.
365    pub(crate) fn dep_path(&self, name: &str) -> Result<DependencyPath> {
366        match self {
367            Self::Member(pinned) => pinned.dep_path(name),
368            Self::Path(pinned) => pinned.dep_path(name),
369            Self::Git(pinned) => pinned.dep_path(name),
370            Self::Ipfs(pinned) => pinned.dep_path(name),
371            Self::Registry(pinned) => pinned.dep_path(name),
372        }
373    }
374
375    /// If the source is associated with a specific semver version, emit it.
376    ///
377    /// Used solely for the package lock file.
378    pub fn semver(&self) -> Option<semver::Version> {
379        match self {
380            Self::Registry(reg) => Some(reg.source.version.clone()),
381            _ => None,
382        }
383    }
384
385    /// Wrap `self` in some type able to be formatted for the compiling output.
386    ///
387    /// This refers to `<source>` in the following:
388    /// ```ignore
389    /// Compiling <kind> <name> (<source>)
390    /// ```
391    pub fn display_compiling<'a>(&'a self, manifest_dir: &'a Path) -> DisplayCompiling<'a, Self> {
392        DisplayCompiling {
393            source: self,
394            manifest_dir,
395        }
396    }
397
398    /// Retrieve the unpinned instance of this source.
399    pub fn unpinned(&self, path: &Path) -> Source {
400        match self {
401            Self::Member(_) => Source::Member(member::Source(path.to_owned())),
402            Self::Git(git) => Source::Git(git.source.clone()),
403            Self::Path(_) => Source::Path(path.to_owned()),
404            Self::Ipfs(ipfs) => Source::Ipfs(ipfs::Source(ipfs.0.clone())),
405            Self::Registry(reg) => Source::Registry(reg.source.clone()),
406        }
407    }
408}
409
410impl<'a> PinCtx<'a> {
411    fn fetch_id(&self) -> FetchId {
412        self.fetch_id
413    }
414    fn path_root(&self) -> PinnedId {
415        self.path_root
416    }
417    fn offline(&self) -> bool {
418        self.offline
419    }
420    fn name(&self) -> &str {
421        self.name
422    }
423    fn ipfs_node(&self) -> &'a IPFSNode {
424        self.ipfs_node
425    }
426}
427
428impl fmt::Display for Pinned {
429    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
430        match self {
431            Self::Member(src) => src.fmt(f),
432            Self::Path(src) => src.fmt(f),
433            Self::Git(src) => src.fmt(f),
434            Self::Ipfs(src) => src.fmt(f),
435            Self::Registry(src) => src.fmt(f),
436        }
437    }
438}
439
440impl fmt::Display for DisplayCompiling<'_, Pinned> {
441    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
442        match self.source {
443            Pinned::Member(_) => self.manifest_dir.display().fmt(f),
444            Pinned::Path(_src) => self.manifest_dir.display().fmt(f),
445            Pinned::Git(src) => src.fmt(f),
446            Pinned::Ipfs(src) => src.fmt(f),
447            Pinned::Registry(src) => src.fmt(f),
448        }
449    }
450}
451
452impl FromStr for Pinned {
453    type Err = PinnedParseError;
454    fn from_str(s: &str) -> Result<Self, Self::Err> {
455        // Also check `"root"` to support reading the legacy `Forc.lock` format and to
456        // avoid breaking old projects.
457        let source = if s == "root" || s == "member" {
458            Self::Member(member::Pinned)
459        } else if let Ok(src) = path::Pinned::from_str(s) {
460            Self::Path(src)
461        } else if let Ok(src) = git::Pinned::from_str(s) {
462            Self::Git(src)
463        } else if let Ok(src) = ipfs::Pinned::from_str(s) {
464            Self::Ipfs(src)
465        } else if let Ok(src) = reg::Pinned::from_str(s) {
466            Self::Registry(src)
467        } else {
468            return Err(PinnedParseError);
469        };
470        Ok(source)
471    }
472}
473
474/// Produce a unique ID for a particular fetch pass.
475///
476/// This is used in the temporary git directory and allows for avoiding contention over the git
477/// repo directory.
478pub fn fetch_id(path: &Path, timestamp: std::time::Instant) -> u64 {
479    let mut hasher = hash_map::DefaultHasher::new();
480    path.hash(&mut hasher);
481    timestamp.hash(&mut hasher);
482    hasher.finish()
483}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488    use crate::manifest::{Dependency, DependencyDetails};
489    use std::collections::BTreeMap;
490
491    /// Helper to create a minimal test manifest file with patch table
492    fn create_test_manifest_file_with_patches(
493        patches: BTreeMap<String, BTreeMap<String, Dependency>>,
494    ) -> (tempfile::TempDir, PackageManifestFile) {
495        // Create a minimal TOML string
496        let mut toml_str = r#"[project]
497name = "test_pkg"
498license = "Apache-2.0"
499entry = "main.sw"
500implicit-std = false
501"#
502        .to_string();
503
504        // Add patches if any
505        if !patches.is_empty() {
506            toml_str.push('\n');
507            for (patch_key, patch_deps) in patches {
508                toml_str.push_str(&format!("[patch.'{}']\n", patch_key));
509                for (dep_name, dep) in patch_deps {
510                    // Manually construct the dependency string
511                    let dep_toml = match dep {
512                        Dependency::Simple(ver) => format!(r#""{ver}""#),
513                        Dependency::Detailed(det) => {
514                            let mut parts = Vec::new();
515                            if let Some(path) = &det.path {
516                                parts.push(format!(r#"path = "{path}""#));
517                            }
518                            if let Some(git) = &det.git {
519                                parts.push(format!(r#"git = "{git}""#));
520                            }
521                            if let Some(branch) = &det.branch {
522                                parts.push(format!(r#"branch = "{branch}""#));
523                            }
524                            if let Some(tag) = &det.tag {
525                                parts.push(format!(r#"tag = "{tag}""#));
526                            }
527                            if let Some(version) = &det.version {
528                                parts.push(format!(r#"version = "{version}""#));
529                            }
530                            format!("{{ {} }}", parts.join(", "))
531                        }
532                    };
533                    toml_str.push_str(&format!("{} = {}\n", dep_name, dep_toml));
534                }
535            }
536        }
537
538        // Create necessary directory structure
539        let temp_dir = tempfile::tempdir().unwrap();
540        let src_dir = temp_dir.path().join("src");
541        std::fs::create_dir(&src_dir).unwrap();
542
543        // Create a minimal main.sw file
544        let main_sw_path = src_dir.join("main.sw");
545        std::fs::write(&main_sw_path, "contract;").unwrap();
546
547        // Write manifest file
548        let manifest_path = temp_dir.path().join("Forc.toml");
549        std::fs::write(&manifest_path, toml_str).unwrap();
550
551        // Read back as PackageManifestFile
552        let manifest_file = PackageManifestFile::from_file(&manifest_path).unwrap();
553
554        (temp_dir, manifest_file)
555    }
556
557    /// Helper to create a path dependency
558    fn path_dep(path: &str) -> Dependency {
559        Dependency::Detailed(DependencyDetails {
560            path: Some(path.to_string()),
561            ..Default::default()
562        })
563    }
564
565    /// Helper to create a git dependency
566    fn git_dep(repo: &str, branch: &str) -> Dependency {
567        Dependency::Detailed(DependencyDetails {
568            git: Some(repo.to_string()),
569            branch: Some(branch.to_string()),
570            ..Default::default()
571        })
572    }
573
574    #[test]
575    fn test_registry_patch_flat_namespace() {
576        // Create a registry source with flat namespace
577        let source = Source::Registry(reg::Source {
578            name: "std".to_string(),
579            version: semver::Version::new(0, 63, 0),
580            namespace: reg::file_location::Namespace::Flat,
581        });
582
583        // Create a manifest with a forc.pub patch
584        let mut patches = BTreeMap::new();
585        let mut forc_pub_patches = BTreeMap::new();
586        forc_pub_patches.insert("std".to_string(), path_dep("../local-std"));
587        patches.insert("forc.pub".to_string(), forc_pub_patches);
588
589        let (_temp_dir, manifest_file) = create_test_manifest_file_with_patches(patches);
590
591        // Test that the patch is found
592        let patch = source.dep_patch("std", &manifest_file).unwrap();
593        assert!(
594            patch.is_some(),
595            "Should find patch for flat namespace registry dependency"
596        );
597
598        let patch = patch.unwrap();
599        match patch {
600            Dependency::Detailed(det) => {
601                assert_eq!(det.path, Some("../local-std".to_string()));
602            }
603            _ => panic!("Expected detailed dependency"),
604        }
605    }
606
607    #[test]
608    fn test_registry_patch_domain_namespace() {
609        // Create a registry source with domain namespace
610        let source = Source::Registry(reg::Source {
611            name: "fuel-core".to_string(),
612            version: semver::Version::new(1, 0, 0),
613            namespace: reg::file_location::Namespace::Domain("com/fuel".to_string()),
614        });
615
616        // Create a manifest with a namespaced patch
617        let mut patches = BTreeMap::new();
618        let mut namespaced_patches = BTreeMap::new();
619        namespaced_patches.insert("fuel-core".to_string(), path_dep("../local-fuel-core"));
620        patches.insert("forc.pub/com/fuel".to_string(), namespaced_patches);
621
622        let (_temp_dir, manifest_file) = create_test_manifest_file_with_patches(patches);
623
624        // Test that the patch is found
625        let patch = source.dep_patch("fuel-core", &manifest_file).unwrap();
626        assert!(
627            patch.is_some(),
628            "Should find patch for domain namespace registry dependency"
629        );
630
631        let patch = patch.unwrap();
632        match patch {
633            Dependency::Detailed(det) => {
634                assert_eq!(det.path, Some("../local-fuel-core".to_string()));
635            }
636            _ => panic!("Expected detailed dependency"),
637        }
638    }
639
640    #[test]
641    fn test_registry_patch_namespace_priority() {
642        // Create a registry source with domain namespace
643        let source = Source::Registry(reg::Source {
644            name: "my-lib".to_string(),
645            version: semver::Version::new(2, 0, 0),
646            namespace: reg::file_location::Namespace::Domain("com/myorg".to_string()),
647        });
648
649        // Create a manifest with BOTH namespaced and generic patches
650        let mut patches = BTreeMap::new();
651
652        // Namespace-specific patch
653        let mut namespaced_patches = BTreeMap::new();
654        namespaced_patches.insert("my-lib".to_string(), path_dep("../namespaced-lib"));
655        patches.insert("forc.pub/com/myorg".to_string(), namespaced_patches);
656
657        // Generic patch
658        let mut generic_patches = BTreeMap::new();
659        generic_patches.insert("my-lib".to_string(), path_dep("../generic-lib"));
660        patches.insert("forc.pub".to_string(), generic_patches);
661
662        let (_temp_dir, manifest_file) = create_test_manifest_file_with_patches(patches);
663
664        // Test that namespace-specific patch takes priority
665        let patch = source.dep_patch("my-lib", &manifest_file).unwrap();
666        assert!(patch.is_some(), "Should find patch");
667
668        let patch = patch.unwrap();
669        match patch {
670            Dependency::Detailed(det) => {
671                assert_eq!(
672                    det.path,
673                    Some("../namespaced-lib".to_string()),
674                    "Should use namespace-specific patch, not generic patch"
675                );
676            }
677            _ => panic!("Expected detailed dependency"),
678        }
679    }
680
681    #[test]
682    fn test_registry_patch_fallback_to_generic() {
683        // Create a registry source with domain namespace
684        let source = Source::Registry(reg::Source {
685            name: "common-lib".to_string(),
686            version: semver::Version::new(1, 0, 0),
687            namespace: reg::file_location::Namespace::Domain("com/myorg".to_string()),
688        });
689
690        // Create a manifest with ONLY generic patch (no namespace-specific)
691        let mut patches = BTreeMap::new();
692        let mut generic_patches = BTreeMap::new();
693        generic_patches.insert("common-lib".to_string(), path_dep("../common-lib"));
694        patches.insert("forc.pub".to_string(), generic_patches);
695
696        let (_temp_dir, manifest_file) = create_test_manifest_file_with_patches(patches);
697
698        // Test that it falls back to generic patch
699        let patch = source.dep_patch("common-lib", &manifest_file).unwrap();
700        assert!(patch.is_some(), "Should find generic patch as fallback");
701
702        let patch = patch.unwrap();
703        match patch {
704            Dependency::Detailed(det) => {
705                assert_eq!(det.path, Some("../common-lib".to_string()));
706            }
707            _ => panic!("Expected detailed dependency"),
708        }
709    }
710
711    #[test]
712    fn test_git_patch_still_works() {
713        // Create a git source
714        let repo_url = "https://github.com/fuellabs/sway";
715        let source = Source::Git(git::Source {
716            repo: git::Url::from_str(repo_url).unwrap(),
717            reference: git::Reference::Tag("v0.63.0".to_string()),
718        });
719
720        // Create a manifest with a git patch
721        let mut patches = BTreeMap::new();
722        let mut git_patches = BTreeMap::new();
723        git_patches.insert(
724            "std".to_string(),
725            git_dep("https://github.com/fuellabs/sway", "feature-branch"),
726        );
727        patches.insert(repo_url.to_string(), git_patches);
728
729        let (_temp_dir, manifest_file) = create_test_manifest_file_with_patches(patches);
730
731        // Test that git patch still works
732        let patch = source.dep_patch("std", &manifest_file).unwrap();
733        assert!(
734            patch.is_some(),
735            "Should find git patch (backward compatibility)"
736        );
737
738        let patch = patch.unwrap();
739        match patch {
740            Dependency::Detailed(det) => {
741                assert_eq!(
742                    det.git,
743                    Some("https://github.com/fuellabs/sway".to_string())
744                );
745                assert_eq!(det.branch, Some("feature-branch".to_string()));
746            }
747            _ => panic!("Expected detailed dependency"),
748        }
749    }
750
751    #[test]
752    fn test_no_patch_found() {
753        // Create a registry source
754        let source = Source::Registry(reg::Source {
755            name: "no-patch-lib".to_string(),
756            version: semver::Version::new(1, 0, 0),
757            namespace: reg::file_location::Namespace::Flat,
758        });
759
760        // Create a manifest with patches for different packages
761        let mut patches = BTreeMap::new();
762        let mut forc_pub_patches = BTreeMap::new();
763        forc_pub_patches.insert("other-lib".to_string(), path_dep("../other-lib"));
764        patches.insert("forc.pub".to_string(), forc_pub_patches);
765
766        let (_temp_dir, manifest_file) = create_test_manifest_file_with_patches(patches);
767
768        // Test that no patch is found
769        let patch = source.dep_patch("no-patch-lib", &manifest_file).unwrap();
770        assert!(
771            patch.is_none(),
772            "Should not find patch for different package"
773        );
774    }
775
776    #[test]
777    fn test_path_source_no_patch() {
778        // Path sources should not have patches
779        let source = Source::Path(PathBuf::from("/some/path"));
780
781        let (_temp_dir, manifest_file) = create_test_manifest_file_with_patches(BTreeMap::new());
782
783        // Test that no patch is found for path sources
784        let patch = source.dep_patch("anything", &manifest_file).unwrap();
785        assert!(patch.is_none(), "Path sources should not support patches");
786    }
787}