Skip to main content

o_/
pm.rs

1use crate::lock::{LockCollector, write_lockfile};
2use crate::report::Report;
3use flate2::read::GzDecoder;
4use home::home_dir;
5use nodejs_semver::{Range, Version};
6use reqwest::blocking::Client;
7use serde::Deserialize;
8use serde_json::Value;
9use ssri::Integrity;
10use std::collections::{HashMap, HashSet};
11use std::fs;
12use std::io;
13use std::path::{Path, PathBuf};
14use tar::Archive;
15use tempfile::tempdir;
16
17#[derive(Debug, Clone, Deserialize)]
18pub struct Manifest {
19    pub name: String,
20    pub version: String,
21    #[serde(default)]
22    pub dependencies: Option<HashMap<String, String>>,
23    #[serde(default, rename = "devDependencies")]
24    pub dev_dependencies: Option<HashMap<String, String>>,
25    #[serde(default, rename = "optionalDependencies")]
26    pub optional_dependencies: Option<HashMap<String, String>>,
27    #[serde(default, rename = "peerDependencies")]
28    pub peer_dependencies: Option<HashMap<String, String>>,
29}
30
31pub fn read_manifest(path: &str) -> io::Result<Manifest> {
32    let manifest_path = find_manifest_path(Path::new(path))?;
33    read_manifest_from_path(&manifest_path)
34}
35
36fn read_manifest_from_path(path: &Path) -> io::Result<Manifest> {
37    let source = fs::read_to_string(path)?;
38    serde_json::from_str(&source).map_err(|source| {
39        io::Error::new(
40            io::ErrorKind::InvalidData,
41            format!("failed to parse {}: {source}", path.display()),
42        )
43    })
44}
45
46fn find_manifest_path(start: &Path) -> io::Result<PathBuf> {
47    let mut current = if start.is_dir() {
48        start.to_path_buf()
49    } else {
50        start
51            .parent()
52            .map(Path::to_path_buf)
53            .unwrap_or_else(|| PathBuf::from("."))
54    };
55
56    loop {
57        let candidate = current.join("package.json");
58        if candidate.is_file() {
59            return Ok(candidate);
60        }
61
62        if !current.pop() {
63            return Err(io::Error::new(
64                io::ErrorKind::NotFound,
65                format!("failed to find package.json from {}", start.display()),
66            ));
67        }
68    }
69}
70
71#[derive(Debug, Deserialize)]
72struct Packument {
73    versions: HashMap<String, RegistryVersion>,
74}
75
76#[derive(Debug, Deserialize)]
77struct RegistryVersion {
78    #[serde(default)]
79    dependencies: HashMap<String, String>,
80    #[serde(default)]
81    optional_dependencies: HashMap<String, String>,
82    #[serde(default)]
83    peer_dependencies: HashMap<String, String>,
84    dist: RegistryDist,
85}
86
87#[derive(Debug, Deserialize)]
88struct RegistryDist {
89    tarball: String,
90    integrity: Option<String>,
91}
92
93#[derive(Debug)]
94struct ResolvedPackage {
95    name: String,
96    version: String,
97    dependencies: HashMap<String, String>,
98    optional_dependencies: HashMap<String, String>,
99    peer_dependencies: HashMap<String, String>,
100    tarball_url: String,
101    integrity: Option<String>,
102}
103
104#[derive(Debug, Clone, Copy)]
105enum DependencyKind {
106    Prod,
107    Dev,
108    Optional,
109    Peer,
110}
111
112#[derive(Debug, Default)]
113struct InstallSummary {
114    prod_installed: usize,
115    dev_installed: usize,
116    optional_installed: usize,
117    peer_installed: usize,
118    warnings: Vec<String>,
119}
120
121impl InstallSummary {
122    fn record_install(&mut self, kind: DependencyKind) {
123        match kind {
124            DependencyKind::Prod => self.prod_installed += 1,
125            DependencyKind::Dev => self.dev_installed += 1,
126            DependencyKind::Optional => self.optional_installed += 1,
127            DependencyKind::Peer => self.peer_installed += 1,
128        }
129    }
130
131    fn warn(&mut self, warning: impl Into<String>) {
132        self.warnings.push(warning.into());
133    }
134}
135
136#[derive(Debug)]
137pub enum PmError {
138    HomeDirUnavailable,
139    MissingGlobalPackageSpec,
140    InvalidPackageSpec {
141        spec: String,
142    },
143    PackageNotInstalled {
144        name: String,
145        path: PathBuf,
146    },
147    FindManifest {
148        start: PathBuf,
149        source: io::Error,
150    },
151    ReadManifest {
152        path: PathBuf,
153        source: io::Error,
154    },
155    ParseManifest {
156        path: PathBuf,
157        source: serde_json::Error,
158    },
159    ProjectRootMissing {
160        path: PathBuf,
161    },
162    CreateDir {
163        path: PathBuf,
164        source: io::Error,
165    },
166    FetchMetadata {
167        package: String,
168        source: reqwest::Error,
169    },
170    MetadataStatus {
171        package: String,
172        source: reqwest::Error,
173    },
174    ReadMetadataBody {
175        package: String,
176        source: reqwest::Error,
177    },
178    ParseMetadata {
179        package: String,
180        source: serde_json::Error,
181    },
182    InvalidRange {
183        package: String,
184        range: String,
185        source: String,
186    },
187    VersionNotFound {
188        package: String,
189        range: String,
190    },
191    MissingResolvedVersion {
192        package: String,
193        version: String,
194    },
195    DownloadTarball {
196        package: String,
197        source: reqwest::Error,
198    },
199    TarballStatus {
200        package: String,
201        source: reqwest::Error,
202    },
203    ReadTarballBody {
204        package: String,
205        source: reqwest::Error,
206    },
207    InvalidIntegrity {
208        package: String,
209        version: String,
210        source: String,
211    },
212    IntegrityMismatch {
213        package: String,
214        version: String,
215        source: String,
216    },
217    ExtractTarball {
218        package: String,
219        source: io::Error,
220    },
221    MissingPackageDir {
222        package: String,
223        path: PathBuf,
224    },
225    RemoveExistingInstall {
226        path: PathBuf,
227        source: io::Error,
228    },
229    CopyInstall {
230        from: PathBuf,
231        to: PathBuf,
232        source: io::Error,
233    },
234    ReadInstalledManifest {
235        path: PathBuf,
236        source: io::Error,
237    },
238    MissingInstalledName {
239        path: PathBuf,
240    },
241    InvalidBinField {
242        path: PathBuf,
243    },
244    InvalidBinEntry {
245        path: PathBuf,
246        entry: String,
247    },
248    AmbiguousBinEntry {
249        package: String,
250        path: PathBuf,
251        available: Vec<String>,
252    },
253    MissingBinTarget {
254        package_dir: PathBuf,
255        target: PathBuf,
256    },
257    CreateTempDir {
258        source: io::Error,
259    },
260    CurrentDir {
261        source: io::Error,
262    },
263    WriteGeneratedManifest {
264        path: PathBuf,
265        source: io::Error,
266    },
267    WriteProcessOutput {
268        source: io::Error,
269    },
270    MissingPackageBinary {
271        package: String,
272        command: String,
273        path: PathBuf,
274    },
275    SpawnPackageBinary {
276        package: String,
277        command: PathBuf,
278        source: io::Error,
279    },
280    PackageBinaryFailed {
281        package: String,
282        command: PathBuf,
283        status: String,
284        stderr: Option<String>,
285    },
286    CreateBinLink {
287        command: String,
288        path: PathBuf,
289        source: io::Error,
290    },
291    RemoveBinLink {
292        command: String,
293        path: PathBuf,
294        source: io::Error,
295    },
296    RemoveInstalledPackage {
297        path: PathBuf,
298        source: io::Error,
299    },
300    ReadLockfile {
301        path: PathBuf,
302        source: io::Error,
303    },
304    ParseLockfile {
305        path: PathBuf,
306        source: serde_json::Error,
307    },
308    WriteLockfile {
309        path: PathBuf,
310        source: io::Error,
311    },
312    InvalidTempPath {
313        path: PathBuf,
314    },
315}
316
317impl PmError {
318    pub fn report(&self) -> Report {
319        match self {
320            Self::HomeDirUnavailable => Report::new("could not resolve home directory")
321                .detail("`$HOME` is unavailable in the current environment"),
322            Self::MissingGlobalPackageSpec => Report::new("global install requires a package name")
323                .detail("example: `o- install --global cowsay`"),
324            Self::InvalidPackageSpec { spec } => Report::new("failed to parse package spec")
325                .detail(format!("spec: {spec}"))
326                .detail("expected `name`, `name@version`, `@scope/name`, or `@scope/name@version`"),
327            Self::PackageNotInstalled { name, path } => {
328                Report::new(format!("package `{name}` is not installed"))
329                    .detail(format!("path: {}", path.display()))
330            }
331            Self::FindManifest { start, source } => Report::new("failed to find package.json")
332                .detail(format!("start: {}", start.display()))
333                .detail(format!("cause: {source}")),
334            Self::ReadManifest { path, source } => Report::new("failed to read package.json")
335                .detail(format!("path: {}", path.display()))
336                .detail(format!("cause: {source}")),
337            Self::ParseManifest { path, source } => Report::new("failed to parse package.json")
338                .detail(format!("path: {}", path.display()))
339                .detail(format!("cause: {source}")),
340            Self::ProjectRootMissing { path } => Report::new("failed to resolve project root")
341                .detail(format!("path: {}", path.display())),
342            Self::CreateDir { path, source } => Report::new("failed to create directory")
343                .detail(format!("path: {}", path.display()))
344                .detail(format!("cause: {source}")),
345            Self::FetchMetadata { package, source } => {
346                Report::new(format!("failed to fetch package metadata for `{package}`"))
347                    .detail(format!("cause: {source}"))
348            }
349            Self::MetadataStatus { package, source } => {
350                Report::new(format!("registry returned an error for `{package}`"))
351                    .detail(format!("cause: {source}"))
352            }
353            Self::ReadMetadataBody { package, source } => Report::new(format!(
354                "failed to read package metadata body for `{package}`"
355            ))
356            .detail(format!("cause: {source}")),
357            Self::ParseMetadata { package, source } => {
358                Report::new(format!("failed to decode package metadata for `{package}`"))
359                    .detail(format!("cause: {source}"))
360            }
361            Self::InvalidRange {
362                package,
363                range,
364                source,
365            } => Report::new(format!("invalid semver range for `{package}`"))
366                .detail(format!("range: {range}"))
367                .detail(format!("cause: {source}")),
368            Self::VersionNotFound { package, range } => Report::new(format!(
369                "no version of `{package}` satisfies the requested range"
370            ))
371            .detail(format!("range: {range}")),
372            Self::MissingResolvedVersion { package, version } => {
373                Report::new(format!("registry metadata is incomplete for `{package}`"))
374                    .detail(format!("version: {version}"))
375            }
376            Self::DownloadTarball { package, source } => {
377                Report::new(format!("failed to download tarball for `{package}`"))
378                    .detail(format!("cause: {source}"))
379            }
380            Self::TarballStatus { package, source } => {
381                Report::new(format!("tarball request failed for `{package}`"))
382                    .detail(format!("cause: {source}"))
383            }
384            Self::ReadTarballBody { package, source } => {
385                Report::new(format!("failed to read tarball body for `{package}`"))
386                    .detail(format!("cause: {source}"))
387            }
388            Self::InvalidIntegrity {
389                package,
390                version,
391                source,
392            } => Report::new(format!("registry integrity is invalid for `{package}`"))
393                .detail(format!("version: {version}"))
394                .detail(format!("cause: {source}")),
395            Self::IntegrityMismatch {
396                package,
397                version,
398                source,
399            } => Report::new(format!("integrity check failed for `{package}`"))
400                .detail(format!("version: {version}"))
401                .detail(format!("cause: {source}")),
402            Self::ExtractTarball { package, source } => {
403                Report::new(format!("failed to extract tarball for `{package}`"))
404                    .detail(format!("cause: {source}"))
405            }
406            Self::MissingPackageDir { package, path } => {
407                Report::new(format!("downloaded tarball for `{package}` is malformed"))
408                    .detail(format!("missing: {}", path.display()))
409            }
410            Self::RemoveExistingInstall { path, source } => {
411                Report::new("failed to remove existing package installation")
412                    .detail(format!("path: {}", path.display()))
413                    .detail(format!("cause: {source}"))
414            }
415            Self::CopyInstall { from, to, source } => Report::new("failed to copy package files")
416                .detail(format!("from: {}", from.display()))
417                .detail(format!("to: {}", to.display()))
418                .detail(format!("cause: {source}")),
419            Self::ReadInstalledManifest { path, source } => {
420                Report::new("failed to read installed package manifest")
421                    .detail(format!("path: {}", path.display()))
422                    .detail(format!("cause: {source}"))
423            }
424            Self::MissingInstalledName { path } => {
425                Report::new("installed package manifest is missing `name`")
426                    .detail(format!("path: {}", path.display()))
427            }
428            Self::InvalidBinField { path } => {
429                Report::new("installed package has an invalid `bin` field")
430                    .detail(format!("path: {}", path.display()))
431            }
432            Self::InvalidBinEntry { path, entry } => {
433                Report::new("installed package has an invalid `bin` entry")
434                    .detail(format!("path: {}", path.display()))
435                    .detail(format!("entry: {entry}"))
436            }
437            Self::AmbiguousBinEntry {
438                package,
439                path,
440                available,
441            } => Report::new(format!("package `{package}` exposes multiple bin commands"))
442                .detail(format!("path: {}", path.display()))
443                .detail(format!("available: {}", available.join(", "))),
444            Self::MissingBinTarget {
445                package_dir,
446                target,
447            } => Report::new("installed package bin target does not exist")
448                .detail(format!("package: {}", package_dir.display()))
449                .detail(format!("target: {}", target.display())),
450            Self::CreateTempDir { source } => Report::new("failed to create temporary directory")
451                .detail(format!("cause: {source}")),
452            Self::CurrentDir { source } => Report::new("failed to resolve current directory")
453                .detail(format!("cause: {source}")),
454            Self::WriteGeneratedManifest { path, source } => {
455                Report::new("failed to write generated package.json")
456                    .detail(format!("path: {}", path.display()))
457                    .detail(format!("cause: {source}"))
458            }
459            Self::WriteProcessOutput { source } => {
460                Report::new("failed to write package output").detail(format!("cause: {source}"))
461            }
462            Self::MissingPackageBinary {
463                package,
464                command,
465                path,
466            } => Report::new(format!(
467                "package `{package}` does not expose a runnable binary"
468            ))
469            .detail(format!("command: {command}"))
470            .detail(format!("expected shim: {}", path.display())),
471            Self::SpawnPackageBinary {
472                package,
473                command,
474                source,
475            } => Report::new(format!("failed to execute package binary for `{package}`"))
476                .detail(format!("command: {}", command.display()))
477                .detail(format!("cause: {source}")),
478            Self::PackageBinaryFailed {
479                package,
480                command,
481                status,
482                stderr,
483            } => {
484                let report = Report::new(format!("package binary `{package}` failed"))
485                    .detail(format!("command: {}", command.display()))
486                    .detail(format!("status: {status}"));
487                if let Some(stderr) = stderr {
488                    report.detail(format!("stderr: {stderr}"))
489                } else {
490                    report
491                }
492            }
493            Self::CreateBinLink {
494                command,
495                path,
496                source,
497            } => Report::new(format!("failed to create bin link `{command}`"))
498                .detail(format!("path: {}", path.display()))
499                .detail(format!("cause: {source}")),
500            Self::RemoveBinLink {
501                command,
502                path,
503                source,
504            } => Report::new(format!("failed to remove bin link `{command}`"))
505                .detail(format!("path: {}", path.display()))
506                .detail(format!("cause: {source}")),
507            Self::RemoveInstalledPackage { path, source } => {
508                Report::new("failed to remove installed package")
509                    .detail(format!("path: {}", path.display()))
510                    .detail(format!("cause: {source}"))
511            }
512            Self::ReadLockfile { path, source } => Report::new("failed to read package-lock.json")
513                .detail(format!("path: {}", path.display()))
514                .detail(format!("cause: {source}")),
515            Self::ParseLockfile { path, source } => {
516                Report::new("failed to parse package-lock.json")
517                    .detail(format!("path: {}", path.display()))
518                    .detail(format!("cause: {source}"))
519            }
520            Self::WriteLockfile { path, source } => {
521                Report::new("failed to write package-lock.json")
522                    .detail(format!("path: {}", path.display()))
523                    .detail(format!("cause: {source}"))
524            }
525            Self::InvalidTempPath { path } => Report::new("failed to run x")
526                .detail("note: tempdir failed".to_string())
527                .detail(format!("path: {}", path.display())),
528        }
529    }
530
531    fn warning_summary(&self) -> String {
532        self.report().summary().to_string()
533    }
534}
535
536pub fn install() -> Result<Report, PmError> {
537    install_from(".")
538}
539
540pub fn global_install(package_spec: Option<&str>) -> Result<Report, PmError> {
541    let package_spec = package_spec.ok_or(PmError::MissingGlobalPackageSpec)?;
542    if package_spec.trim().is_empty() {
543        return Err(PmError::MissingGlobalPackageSpec);
544    }
545    let (package_name, package_range) = parse_package_spec(package_spec);
546    let global_root = global_packages_root()?;
547    let node_modules = global_node_modules_dir(&global_root);
548    fs::create_dir_all(&node_modules).map_err(|source| PmError::CreateDir {
549        path: node_modules.clone(),
550        source,
551    })?;
552
553    let mut installed = HashSet::new();
554    let mut lock = LockCollector::new();
555    let mut root_dependencies = HashMap::new();
556    root_dependencies.insert(package_name.clone(), package_range.clone());
557    let empty_dependencies = HashMap::new();
558    lock.insert_root_fields(
559        "o--global",
560        "0.0.0",
561        &root_dependencies,
562        &empty_dependencies,
563        &empty_dependencies,
564        &empty_dependencies,
565    );
566
567    let mut summary = InstallSummary::default();
568    let client = Client::new();
569    install_dependency(
570        &client,
571        &global_root,
572        &package_name,
573        &package_range,
574        &node_modules,
575        &mut installed,
576        &mut lock,
577        &mut summary,
578        DependencyKind::Prod,
579    )?;
580
581    let installed_manifest_path = install_dir(&node_modules, &package_name).join("package.json");
582    let installed_manifest = read_manifest_from_path(&installed_manifest_path)
583        .map_err(|source| map_manifest_error(&installed_manifest_path, source))?;
584    let lockfile = lock.into_lockfile_fields("o--global", "0.0.0");
585    let lockfile_path =
586        write_lockfile(&global_root, &lockfile).map_err(|source| PmError::WriteLockfile {
587            path: global_root.join("package-lock.json"),
588            source,
589        })?;
590
591    Ok(Report::new(format!(
592        "installed global package `{}`",
593        installed_manifest.name
594    ))
595    .detail(format!("requested: {package_spec}"))
596    .detail(format!("resolved version: {}", installed_manifest.version))
597    .detail(format!("root: {}", global_root.display()))
598    .detail(format!(
599        "bin dir: {}",
600        global_bin_dir(&node_modules).display()
601    ))
602    .detail(format!("lockfile: {}", lockfile_path.display()))
603    .detail(format!("dependencies: {}", summary.prod_installed))
604    .detail(format!(
605        "optionalDependencies: {}",
606        summary.optional_installed
607    ))
608    .detail(format!("peerDependencies: {}", summary.peer_installed))
609    .detail(format!("peer warnings: {}", summary.warnings.len()))
610    .detail(if summary.warnings.is_empty() {
611        "peer/optional warnings: none".to_string()
612    } else {
613        format!("peer/optional warnings: {}", summary.warnings.join(" | "))
614    }))
615}
616
617pub fn install_from(path: &str) -> Result<Report, PmError> {
618    let manifest_path =
619        find_manifest_path(Path::new(path)).map_err(|source| PmError::FindManifest {
620            start: PathBuf::from(path),
621            source,
622        })?;
623    let project_root = manifest_path
624        .parent()
625        .ok_or_else(|| PmError::ProjectRootMissing {
626            path: manifest_path.clone(),
627        })?;
628    let manifest = read_manifest_from_path(&manifest_path)
629        .map_err(|source| map_manifest_error(&manifest_path, source))?;
630    let node_modules = project_root.join("node_modules");
631    fs::create_dir_all(&node_modules).map_err(|source| PmError::CreateDir {
632        path: node_modules.clone(),
633        source,
634    })?;
635
636    let mut installed = HashSet::new();
637    let mut lock = LockCollector::new();
638    lock.insert_root_fields(
639        &manifest.name,
640        &manifest.version,
641        &manifest.dependencies.clone().unwrap_or_default(),
642        &manifest.dev_dependencies.clone().unwrap_or_default(),
643        &manifest.optional_dependencies.clone().unwrap_or_default(),
644        &manifest.peer_dependencies.clone().unwrap_or_default(),
645    );
646    let mut summary = InstallSummary::default();
647    let client = Client::new();
648
649    let root_dependencies = manifest.dependencies.clone().unwrap_or_default();
650    let root_dev_dependencies = manifest.dev_dependencies.clone().unwrap_or_default();
651    let root_optional_dependencies = manifest.optional_dependencies.clone().unwrap_or_default();
652    let root_peer_dependencies = manifest.peer_dependencies.clone().unwrap_or_default();
653
654    install_dependency_set(
655        &client,
656        project_root,
657        &root_dependencies,
658        &node_modules,
659        &mut installed,
660        &mut lock,
661        &mut summary,
662        DependencyKind::Prod,
663    )?;
664    install_dependency_set(
665        &client,
666        project_root,
667        &root_dev_dependencies,
668        &node_modules,
669        &mut installed,
670        &mut lock,
671        &mut summary,
672        DependencyKind::Dev,
673    )?;
674    install_optional_dependency_set(
675        &client,
676        project_root,
677        &root_optional_dependencies,
678        &node_modules,
679        &mut installed,
680        &mut lock,
681        &mut summary,
682        "root package",
683    );
684    reconcile_peer_dependencies(
685        "root package",
686        &root_peer_dependencies,
687        &client,
688        project_root,
689        &node_modules,
690        &mut installed,
691        &mut lock,
692        &mut summary,
693    );
694
695    let lockfile = lock.into_lockfile_fields(&manifest.name, &manifest.version);
696    let lockfile_path =
697        write_lockfile(project_root, &lockfile).map_err(|source| PmError::WriteLockfile {
698            path: project_root.join("package-lock.json"),
699            source,
700        })?;
701
702    Ok(Report::new("installed project dependencies")
703        .detail(format!("root: {}", project_root.display()))
704        .detail(format!("dependencies: {}", summary.prod_installed))
705        .detail(format!("devDependencies: {}", summary.dev_installed))
706        .detail(format!(
707            "optionalDependencies: {}",
708            summary.optional_installed
709        ))
710        .detail(format!("peerDependencies: {}", summary.peer_installed))
711        .detail(format!("peer warnings: {}", summary.warnings.len()))
712        .detail(format!("lockfile: {}", lockfile_path.display()))
713        .detail(format!(
714            "declared dependencies: {}",
715            root_dependencies.len()
716        ))
717        .detail(format!(
718            "declared devDependencies: {}",
719            root_dev_dependencies.len()
720        ))
721        .detail(format!(
722            "declared optionalDependencies: {}",
723            root_optional_dependencies.len()
724        ))
725        .detail(format!(
726            "declared peerDependencies: {}",
727            root_peer_dependencies.len()
728        ))
729        .detail(if summary.warnings.is_empty() {
730            "peer/optional warnings: none".to_string()
731        } else {
732            format!("peer/optional warnings: {}", summary.warnings.join(" | "))
733        }))
734}
735
736fn install_dependency_set(
737    client: &Client,
738    project_root: &Path,
739    dependencies: &HashMap<String, String>,
740    node_modules_dir: &Path,
741    installed: &mut HashSet<String>,
742    lock: &mut LockCollector,
743    summary: &mut InstallSummary,
744    kind: DependencyKind,
745) -> Result<(), PmError> {
746    for (name, range) in dependencies {
747        install_dependency(
748            client,
749            project_root,
750            name,
751            range,
752            node_modules_dir,
753            installed,
754            lock,
755            summary,
756            kind,
757        )?;
758    }
759
760    Ok(())
761}
762
763fn install_optional_dependency_set(
764    client: &Client,
765    project_root: &Path,
766    dependencies: &HashMap<String, String>,
767    node_modules_dir: &Path,
768    installed: &mut HashSet<String>,
769    lock: &mut LockCollector,
770    summary: &mut InstallSummary,
771    owner: &str,
772) {
773    for (name, range) in dependencies {
774        if let Err(error) = install_dependency(
775            client,
776            project_root,
777            name,
778            range,
779            node_modules_dir,
780            installed,
781            lock,
782            summary,
783            DependencyKind::Optional,
784        ) {
785            summary.warn(format!(
786                "optional dependency `{name}` for `{owner}` was skipped: {}",
787                error.warning_summary()
788            ));
789        }
790    }
791}
792
793fn install_dependency(
794    client: &Client,
795    project_root: &Path,
796    name: &str,
797    range: &str,
798    node_modules_dir: &Path,
799    installed: &mut HashSet<String>,
800    lock: &mut LockCollector,
801    summary: &mut InstallSummary,
802    kind: DependencyKind,
803) -> Result<(), PmError> {
804    let resolved = resolve_package(client, name, range)?;
805    let install_key = format!(
806        "{}@{}::{}",
807        resolved.name,
808        resolved.version,
809        node_modules_dir.display()
810    );
811
812    if !installed.insert(install_key) {
813        return Ok(());
814    }
815
816    let target_dir = install_dir(node_modules_dir, &resolved.name);
817    if is_matching_install(&target_dir, &resolved.version)? {
818        lock.insert_package(
819            project_root,
820            &target_dir,
821            &resolved.name,
822            &resolved.version,
823            &resolved.tarball_url,
824            resolved.integrity.as_deref(),
825            &resolved.dependencies,
826            &resolved.optional_dependencies,
827            &resolved.peer_dependencies,
828        )
829        .map_err(|source| PmError::WriteLockfile {
830            path: project_root.join("package-lock.json"),
831            source,
832        })?;
833        install_dependency_set(
834            client,
835            project_root,
836            &resolved.dependencies,
837            &target_dir.join("node_modules"),
838            installed,
839            lock,
840            summary,
841            DependencyKind::Prod,
842        )?;
843        install_optional_dependency_set(
844            client,
845            project_root,
846            &resolved.optional_dependencies,
847            &target_dir.join("node_modules"),
848            installed,
849            lock,
850            summary,
851            &resolved.name,
852        );
853        reconcile_peer_dependencies(
854            &resolved.name,
855            &resolved.peer_dependencies,
856            client,
857            project_root,
858            node_modules_dir,
859            installed,
860            lock,
861            summary,
862        );
863        return Ok(());
864    }
865
866    if let Some(parent) = target_dir.parent() {
867        fs::create_dir_all(parent).map_err(|source| PmError::CreateDir {
868            path: parent.to_path_buf(),
869            source,
870        })?;
871    }
872
873    let package_root = download_and_extract_package(client, &resolved)?;
874
875    if target_dir.exists() {
876        fs::remove_dir_all(&target_dir).map_err(|source| PmError::RemoveExistingInstall {
877            path: target_dir.clone(),
878            source,
879        })?;
880    }
881    copy_dir_all(&package_root, &target_dir).map_err(|source| PmError::CopyInstall {
882        from: package_root.clone(),
883        to: target_dir.clone(),
884        source,
885    })?;
886    create_bin_links(node_modules_dir, &target_dir)?;
887    summary.record_install(kind);
888    lock.insert_package(
889        project_root,
890        &target_dir,
891        &resolved.name,
892        &resolved.version,
893        &resolved.tarball_url,
894        resolved.integrity.as_deref(),
895        &resolved.dependencies,
896        &resolved.optional_dependencies,
897        &resolved.peer_dependencies,
898    )
899    .map_err(|source| PmError::WriteLockfile {
900        path: project_root.join("package-lock.json"),
901        source,
902    })?;
903
904    let nested_node_modules = target_dir.join("node_modules");
905    fs::create_dir_all(&nested_node_modules).map_err(|source| PmError::CreateDir {
906        path: nested_node_modules.clone(),
907        source,
908    })?;
909    install_dependency_set(
910        client,
911        project_root,
912        &resolved.dependencies,
913        &nested_node_modules,
914        installed,
915        lock,
916        summary,
917        DependencyKind::Prod,
918    )?;
919    install_optional_dependency_set(
920        client,
921        project_root,
922        &resolved.optional_dependencies,
923        &nested_node_modules,
924        installed,
925        lock,
926        summary,
927        &resolved.name,
928    );
929    reconcile_peer_dependencies(
930        &resolved.name,
931        &resolved.peer_dependencies,
932        client,
933        project_root,
934        node_modules_dir,
935        installed,
936        lock,
937        summary,
938    );
939
940    Ok(())
941}
942
943fn resolve_package(client: &Client, name: &str, range: &str) -> Result<ResolvedPackage, PmError> {
944    let url = resolve_npm_url(name);
945    let response = client
946        .get(url)
947        .send()
948        .map_err(|source| PmError::FetchMetadata {
949            package: name.to_string(),
950            source,
951        })?;
952
953    let response = response
954        .error_for_status()
955        .map_err(|source| PmError::MetadataStatus {
956            package: name.to_string(),
957            source,
958        })?;
959
960    let body = response
961        .text()
962        .map_err(|source| PmError::ReadMetadataBody {
963            package: name.to_string(),
964            source,
965        })?;
966
967    let packument: Packument =
968        serde_json::from_str(&body).map_err(|source| PmError::ParseMetadata {
969            package: name.to_string(),
970            source,
971        })?;
972
973    let range: Range = range
974        .parse()
975        .map_err(|source: nodejs_semver::SemverError| PmError::InvalidRange {
976            package: name.to_string(),
977            range: range.to_string(),
978            source: source.to_string(),
979        })?;
980
981    let version = packument
982        .versions
983        .keys()
984        .filter_map(|raw_version| {
985            Version::parse(raw_version)
986                .ok()
987                .map(|parsed| (raw_version, parsed))
988        })
989        .filter(|(_, parsed)| parsed.satisfies(&range))
990        .map(|(_, parsed)| parsed)
991        .max()
992        .ok_or_else(|| PmError::VersionNotFound {
993            package: name.to_string(),
994            range: range.to_string(),
995        })?;
996
997    let version_string = version.to_string();
998    let metadata =
999        packument
1000            .versions
1001            .get(&version_string)
1002            .ok_or_else(|| PmError::MissingResolvedVersion {
1003                package: name.to_string(),
1004                version: version_string.clone(),
1005            })?;
1006
1007    Ok(ResolvedPackage {
1008        name: name.to_string(),
1009        version: version_string,
1010        dependencies: metadata.dependencies.clone(),
1011        optional_dependencies: metadata.optional_dependencies.clone(),
1012        peer_dependencies: metadata.peer_dependencies.clone(),
1013        tarball_url: metadata.dist.tarball.clone(),
1014        integrity: metadata.dist.integrity.clone(),
1015    })
1016}
1017
1018fn download_and_extract_package(
1019    client: &Client,
1020    package: &ResolvedPackage,
1021) -> Result<PathBuf, PmError> {
1022    let response =
1023        client
1024            .get(&package.tarball_url)
1025            .send()
1026            .map_err(|source| PmError::DownloadTarball {
1027                package: package.name.clone(),
1028                source,
1029            })?;
1030    let response = response
1031        .error_for_status()
1032        .map_err(|source| PmError::TarballStatus {
1033            package: package.name.clone(),
1034            source,
1035        })?;
1036
1037    let bytes = response
1038        .bytes()
1039        .map_err(|source| PmError::ReadTarballBody {
1040            package: package.name.clone(),
1041            source,
1042        })?;
1043    verify_integrity(package, bytes.as_ref())?;
1044
1045    let temp = tempdir().map_err(|source| PmError::ExtractTarball {
1046        package: package.name.clone(),
1047        source,
1048    })?;
1049    let temp_path = temp.keep();
1050
1051    let tar = GzDecoder::new(bytes.as_ref());
1052    let mut archive = Archive::new(tar);
1053    archive
1054        .unpack(&temp_path)
1055        .map_err(|source| PmError::ExtractTarball {
1056            package: package.name.clone(),
1057            source,
1058        })?;
1059
1060    let package_root = temp_path.join("package");
1061    if !package_root.is_dir() {
1062        return Err(PmError::MissingPackageDir {
1063            package: package.name.clone(),
1064            path: package_root,
1065        });
1066    }
1067
1068    Ok(package_root)
1069}
1070
1071fn verify_integrity(package: &ResolvedPackage, bytes: &[u8]) -> Result<(), PmError> {
1072    let Some(integrity) = &package.integrity else {
1073        return Ok(());
1074    };
1075
1076    let parsed: Integrity =
1077        integrity
1078            .parse()
1079            .map_err(|source: ssri::Error| PmError::InvalidIntegrity {
1080                package: package.name.clone(),
1081                version: package.version.clone(),
1082                source: source.to_string(),
1083            })?;
1084
1085    parsed
1086        .check(bytes)
1087        .map_err(|source: ssri::Error| PmError::IntegrityMismatch {
1088            package: package.name.clone(),
1089            version: package.version.clone(),
1090            source: source.to_string(),
1091        })?;
1092
1093    Ok(())
1094}
1095
1096fn install_dir(node_modules: &Path, package_name: &str) -> PathBuf {
1097    if let Some((scope, name)) = package_name.split_once('/') {
1098        node_modules.join(scope).join(name)
1099    } else {
1100        node_modules.join(package_name)
1101    }
1102}
1103
1104fn is_matching_install(path: &Path, version: &str) -> Result<bool, PmError> {
1105    let manifest_path = path.join("package.json");
1106    if !manifest_path.is_file() {
1107        return Ok(false);
1108    }
1109
1110    let manifest = read_manifest_from_path(&manifest_path)
1111        .map_err(|source| map_manifest_error(&manifest_path, source))?;
1112    Ok(manifest.version == version)
1113}
1114
1115fn copy_dir_all(src: &Path, dst: &Path) -> io::Result<()> {
1116    fs::create_dir_all(dst)?;
1117
1118    for entry in fs::read_dir(src)? {
1119        let entry = entry?;
1120        let file_type = entry.file_type()?;
1121        let from = entry.path();
1122        let to = dst.join(entry.file_name());
1123
1124        if file_type.is_dir() {
1125            copy_dir_all(&from, &to)?;
1126        } else {
1127            fs::copy(&from, &to)?;
1128        }
1129    }
1130
1131    Ok(())
1132}
1133
1134fn create_bin_links(node_modules_dir: &Path, package_dir: &Path) -> Result<(), PmError> {
1135    let bin_entries = read_bin_entries(package_dir)?;
1136    if bin_entries.is_empty() {
1137        return Ok(());
1138    }
1139
1140    let bin_dir = node_modules_dir.join(".bin");
1141    fs::create_dir_all(&bin_dir).map_err(|source| PmError::CreateDir {
1142        path: bin_dir.clone(),
1143        source,
1144    })?;
1145
1146    for (command_name, relative_target) in bin_entries {
1147        let target = package_dir.join(normalize_package_relative_path(&relative_target));
1148        if !target.is_file() {
1149            return Err(PmError::MissingBinTarget {
1150                package_dir: package_dir.to_path_buf(),
1151                target,
1152            });
1153        }
1154
1155        create_bin_link(&bin_dir, &command_name, &target)?;
1156    }
1157
1158    Ok(())
1159}
1160
1161fn read_bin_entries(package_dir: &Path) -> Result<Vec<(String, String)>, PmError> {
1162    let package_json_path = package_dir.join("package.json");
1163    let source = fs::read_to_string(&package_json_path).map_err(|source| {
1164        PmError::ReadInstalledManifest {
1165            path: package_json_path.clone(),
1166            source,
1167        }
1168    })?;
1169    let value: Value = serde_json::from_str(&source).map_err(|source| PmError::ParseManifest {
1170        path: package_json_path.clone(),
1171        source,
1172    })?;
1173
1174    let package_name = value
1175        .get("name")
1176        .and_then(Value::as_str)
1177        .map(default_bin_name)
1178        .ok_or_else(|| PmError::MissingInstalledName {
1179            path: package_json_path.clone(),
1180        })?;
1181
1182    let Some(bin_value) = value.get("bin") else {
1183        return Ok(Vec::new());
1184    };
1185
1186    match bin_value {
1187        Value::String(path) => Ok(vec![(package_name, path.clone())]),
1188        Value::Object(entries) => {
1189            let mut bins = Vec::with_capacity(entries.len());
1190            for (command_name, target) in entries {
1191                let target = target.as_str().ok_or_else(|| PmError::InvalidBinEntry {
1192                    path: package_json_path.clone(),
1193                    entry: command_name.clone(),
1194                })?;
1195                bins.push((command_name.clone(), target.to_string()));
1196            }
1197            Ok(bins)
1198        }
1199        Value::Null => Ok(Vec::new()),
1200        _ => Err(PmError::InvalidBinField {
1201            path: package_json_path,
1202        }),
1203    }
1204}
1205
1206fn default_bin_name(package_name: &str) -> String {
1207    package_name
1208        .rsplit_once('/')
1209        .map(|(_, name)| name.to_string())
1210        .unwrap_or_else(|| package_name.to_string())
1211}
1212
1213fn normalize_package_relative_path(path: &str) -> PathBuf {
1214    let trimmed = path.strip_prefix("./").unwrap_or(path);
1215    PathBuf::from(trimmed)
1216}
1217
1218fn reconcile_peer_dependencies(
1219    owner: &str,
1220    peer_dependencies: &HashMap<String, String>,
1221    client: &Client,
1222    project_root: &Path,
1223    node_modules_dir: &Path,
1224    installed: &mut HashSet<String>,
1225    lock: &mut LockCollector,
1226    summary: &mut InstallSummary,
1227) {
1228    for (name, range) in peer_dependencies {
1229        if peer_dependency_warning(name, range, node_modules_dir).is_some() {
1230            if let Err(error) = install_dependency(
1231                client,
1232                project_root,
1233                name,
1234                range,
1235                node_modules_dir,
1236                installed,
1237                lock,
1238                summary,
1239                DependencyKind::Peer,
1240            ) {
1241                summary.warn(format!(
1242                    "peer dependency `{name}` for `{owner}` could not be installed: {}",
1243                    error.warning_summary()
1244                ));
1245            }
1246        }
1247    }
1248
1249    validate_peer_dependencies(owner, peer_dependencies, node_modules_dir, summary);
1250}
1251
1252fn validate_peer_dependencies(
1253    owner: &str,
1254    peer_dependencies: &HashMap<String, String>,
1255    node_modules_dir: &Path,
1256    summary: &mut InstallSummary,
1257) {
1258    for (name, range) in peer_dependencies {
1259        if let Some(warning) = peer_dependency_warning(name, range, node_modules_dir) {
1260            summary.warn(format!("peer dependency for `{owner}`: {warning}"));
1261        }
1262    }
1263}
1264
1265fn peer_dependency_warning(name: &str, range: &str, node_modules_dir: &Path) -> Option<String> {
1266    let package_dir = install_dir(node_modules_dir, name);
1267    let manifest_path = package_dir.join("package.json");
1268    if !manifest_path.is_file() {
1269        return Some(format!("missing `{name}` required by range `{range}`"));
1270    }
1271
1272    let manifest = match read_manifest_from_path(&manifest_path) {
1273        Ok(manifest) => manifest,
1274        Err(error) => {
1275            return Some(format!(
1276                "failed to read installed `{name}` manifest: {error}"
1277            ));
1278        }
1279    };
1280    let installed_version = match Version::parse(&manifest.version) {
1281        Ok(version) => version,
1282        Err(error) => {
1283            return Some(format!(
1284                "`{name}` is installed with invalid version `{}`: {error}",
1285                manifest.version
1286            ));
1287        }
1288    };
1289    let expected_range: Range = match range.parse() {
1290        Ok(parsed) => parsed,
1291        Err(error) => {
1292            return Some(format!(
1293                "`{name}` requires invalid peer range `{range}`: {error}"
1294            ));
1295        }
1296    };
1297    if installed_version.satisfies(&expected_range) {
1298        None
1299    } else {
1300        Some(format!(
1301            "`{name}` is installed as `{}` but `{range}` is required",
1302            manifest.version
1303        ))
1304    }
1305}
1306
1307#[cfg(unix)]
1308fn create_bin_link(bin_dir: &Path, command_name: &str, target: &Path) -> Result<(), PmError> {
1309    use std::os::unix::fs::symlink;
1310
1311    let link_path = bin_dir.join(command_name);
1312    remove_existing_link_path(&link_path).map_err(|source| PmError::CreateBinLink {
1313        command: command_name.to_string(),
1314        path: link_path.clone(),
1315        source,
1316    })?;
1317    symlink(target, &link_path).map_err(|source| PmError::CreateBinLink {
1318        command: command_name.to_string(),
1319        path: link_path,
1320        source,
1321    })
1322}
1323
1324#[cfg(windows)]
1325fn create_bin_link(bin_dir: &Path, command_name: &str, target: &Path) -> Result<(), PmError> {
1326    let link_path = bin_dir.join(format!("{command_name}.cmd"));
1327    remove_existing_link_path(&link_path).map_err(|source| PmError::CreateBinLink {
1328        command: command_name.to_string(),
1329        path: link_path.clone(),
1330        source,
1331    })?;
1332    let script = format!("@ECHO off\r\nnode \"{}\" %*\r\n", target.display());
1333    fs::write(&link_path, script).map_err(|source| PmError::CreateBinLink {
1334        command: command_name.to_string(),
1335        path: link_path,
1336        source,
1337    })
1338}
1339
1340fn remove_existing_link_path(path: &Path) -> io::Result<()> {
1341    if !path.exists() {
1342        return Ok(());
1343    }
1344
1345    let metadata = fs::symlink_metadata(path)?;
1346    if metadata.file_type().is_dir() && !metadata.file_type().is_symlink() {
1347        fs::remove_dir_all(path)
1348    } else {
1349        fs::remove_file(path)
1350    }
1351}
1352
1353fn resolve_npm_url(package: &str) -> String {
1354    let encoded = package.replace('@', "%40").replace('/', "%2F");
1355    format!("https://registry.npmjs.org/{encoded}")
1356}
1357
1358fn global_packages_root() -> Result<PathBuf, PmError> {
1359    let mut path = home_dir().ok_or(PmError::HomeDirUnavailable)?;
1360    path.push(".config");
1361    path.push("o-");
1362    path.push("packages");
1363    Ok(path)
1364}
1365
1366fn global_node_modules_dir(global_root: &Path) -> PathBuf {
1367    global_root.join("node_modules")
1368}
1369
1370fn global_bin_dir(node_modules_dir: &Path) -> PathBuf {
1371    node_modules_dir.join(".bin")
1372}
1373
1374fn parse_package_spec(spec: &str) -> (String, String) {
1375    let trimmed = spec.trim();
1376    if trimmed.is_empty() {
1377        return (String::new(), "*".to_string());
1378    }
1379
1380    if let Some((name, range)) = split_package_spec(trimmed) {
1381        return (name.to_string(), normalize_package_range(range).to_string());
1382    }
1383
1384    (trimmed.to_string(), "*".to_string())
1385}
1386
1387fn split_package_spec(spec: &str) -> Option<(&str, &str)> {
1388    if spec.starts_with('@') {
1389        let slash = spec.find('/')?;
1390        let tail = &spec[slash + 1..];
1391        let at = tail.rfind('@')?;
1392        let split_index = slash + 1 + at;
1393        let name = &spec[..split_index];
1394        let range = &spec[split_index + 1..];
1395        if range.is_empty() {
1396            None
1397        } else {
1398            Some((name, range))
1399        }
1400    } else if let Some((name, range)) = spec.rsplit_once('@') {
1401        if name.is_empty() || range.is_empty() {
1402            None
1403        } else {
1404            Some((name, range))
1405        }
1406    } else {
1407        None
1408    }
1409}
1410
1411fn normalize_package_range(range: &str) -> &str {
1412    if range == "latest" { "*" } else { range }
1413}
1414
1415pub fn remove_shim(node_modules_dir: &Path, command: &str) -> Result<bool, PmError> {
1416    let shim_path = shim_path_for_command(node_modules_dir, command);
1417    if !shim_path.exists() {
1418        return Ok(false);
1419    }
1420
1421    remove_existing_link_path(&shim_path).map_err(|source| PmError::RemoveBinLink {
1422        command: command.to_string(),
1423        path: shim_path,
1424        source,
1425    })?;
1426    Ok(true)
1427}
1428
1429pub fn uninstall(name: &str) -> Result<Report, PmError> {
1430    let global_root = global_packages_root()?;
1431    let node_modules_dir = global_node_modules_dir(&global_root);
1432    let package_dir = install_dir(&node_modules_dir, name);
1433    if !package_dir.is_dir() {
1434        return Err(PmError::PackageNotInstalled {
1435            name: name.to_string(),
1436            path: package_dir,
1437        });
1438    }
1439
1440    let manifest_path = package_dir.join("package.json");
1441    let manifest = read_manifest_from_path(&manifest_path)
1442        .map_err(|source| map_manifest_error(&manifest_path, source))?;
1443    let bin_entries = read_bin_entries(&package_dir)?;
1444
1445    fs::remove_dir_all(&package_dir).map_err(|source| PmError::RemoveInstalledPackage {
1446        path: package_dir.clone(),
1447        source,
1448    })?;
1449    remove_empty_scope_dir(&package_dir)?;
1450
1451    let mut removed_shims = Vec::new();
1452    for (command, _) in bin_entries {
1453        if remove_shim(&node_modules_dir, &command)? {
1454            removed_shims.push(command);
1455        }
1456    }
1457
1458    let lockfile_path = remove_global_lockfile_entry(&global_root, &package_dir, &manifest.name)?;
1459
1460    Ok(
1461        Report::new(format!("uninstalled package `{}`", manifest.name))
1462            .detail(format!("version: {}", manifest.version))
1463            .detail(format!("package: {}", package_dir.display()))
1464            .detail(format!(
1465                "bin dir: {}",
1466                global_bin_dir(&node_modules_dir).display()
1467            ))
1468            .detail(match lockfile_path {
1469                Some(path) => format!("lockfile: {}", path.display()),
1470                None => "lockfile: none".to_string(),
1471            })
1472            .detail(if removed_shims.is_empty() {
1473                "removed shims: none".to_string()
1474            } else {
1475                format!("removed shims: {}", removed_shims.join(", "))
1476            }),
1477    )
1478}
1479
1480#[cfg(unix)]
1481fn shim_path_for_command(node_modules_dir: &Path, command: &str) -> PathBuf {
1482    global_bin_dir(node_modules_dir).join(command)
1483}
1484
1485#[cfg(windows)]
1486fn shim_path_for_command(node_modules_dir: &Path, command: &str) -> PathBuf {
1487    global_bin_dir(node_modules_dir).join(format!("{command}.cmd"))
1488}
1489
1490fn remove_empty_scope_dir(package_dir: &Path) -> Result<(), PmError> {
1491    let Some(parent) = package_dir.parent() else {
1492        return Ok(());
1493    };
1494
1495    let Some(scope_name) = parent.file_name().and_then(|name| name.to_str()) else {
1496        return Ok(());
1497    };
1498
1499    if !scope_name.starts_with('@') {
1500        return Ok(());
1501    }
1502
1503    if parent
1504        .read_dir()
1505        .map_err(|source| PmError::RemoveInstalledPackage {
1506            path: parent.to_path_buf(),
1507            source,
1508        })?
1509        .next()
1510        .is_none()
1511    {
1512        fs::remove_dir(parent).map_err(|source| PmError::RemoveInstalledPackage {
1513            path: parent.to_path_buf(),
1514            source,
1515        })?;
1516    }
1517
1518    Ok(())
1519}
1520
1521fn remove_global_lockfile_entry(
1522    global_root: &Path,
1523    package_dir: &Path,
1524    package_name: &str,
1525) -> Result<Option<PathBuf>, PmError> {
1526    let lockfile_path = global_root.join("package-lock.json");
1527    if !lockfile_path.is_file() {
1528        return Ok(None);
1529    }
1530
1531    let source = fs::read_to_string(&lockfile_path).map_err(|source| PmError::ReadLockfile {
1532        path: lockfile_path.clone(),
1533        source,
1534    })?;
1535    let mut lockfile: crate::lock::LockFile =
1536        serde_json::from_str(&source).map_err(|source| PmError::ParseLockfile {
1537            path: lockfile_path.clone(),
1538            source,
1539        })?;
1540
1541    let key = package_dir
1542        .strip_prefix(global_root)
1543        .map(|path| path.to_string_lossy().replace('\\', "/"))
1544        .unwrap_or_else(|_| package_dir.to_string_lossy().replace('\\', "/"));
1545    lockfile.packages.remove(&key);
1546
1547    if let Some(root) = lockfile.packages.get_mut("") {
1548        remove_dependency_entry(&mut root.dependencies, package_name);
1549        remove_dependency_entry(&mut root.dev_dependencies, package_name);
1550        remove_dependency_entry(&mut root.optional_dependencies, package_name);
1551        remove_dependency_entry(&mut root.peer_dependencies, package_name);
1552    }
1553
1554    let rewritten =
1555        write_lockfile(global_root, &lockfile).map_err(|source| PmError::WriteLockfile {
1556            path: lockfile_path,
1557            source,
1558        })?;
1559    Ok(Some(rewritten))
1560}
1561
1562fn remove_dependency_entry(
1563    dependencies: &mut Option<std::collections::BTreeMap<String, String>>,
1564    name: &str,
1565) {
1566    let should_clear = if let Some(entries) = dependencies.as_mut() {
1567        entries.remove(name);
1568        entries.is_empty()
1569    } else {
1570        false
1571    };
1572
1573    if should_clear {
1574        *dependencies = None;
1575    }
1576}
1577
1578fn map_manifest_error(path: &Path, source: io::Error) -> PmError {
1579    match source.kind() {
1580        io::ErrorKind::InvalidData => {
1581            let parse_source =
1582                serde_json::Error::io(io::Error::new(source.kind(), source.to_string()));
1583            PmError::ParseManifest {
1584                path: path.to_path_buf(),
1585                source: parse_source,
1586            }
1587        }
1588        _ => PmError::ReadManifest {
1589            path: path.to_path_buf(),
1590            source,
1591        },
1592    }
1593}