1use std::fmt::Display;
2use std::path::PathBuf;
3use std::{ffi::OsStr, path::Path};
4
5use anyhow::{Context, Result, bail};
6
7mod crystal;
8mod dart;
9mod go;
10mod javascript;
11mod php;
12mod python;
13mod ruby;
14mod rust;
15use crystal::parse_shard_lock;
16use dart::parse_pubspec_lock;
17use go::parse_go_sum;
18use javascript::{parse_npm_lock, parse_yarn_lock};
19use php::parse_composer_lock;
20use python::parse_requirements_txt;
21use ruby::parse_gemfile_lock;
22use serde::Deserialize;
23
24#[derive(PartialEq, Eq, Debug, Deserialize)]
25struct Package {
29 name: String,
30 version: String,
31}
32
33impl Package {
34 fn new(name: &str, version: &str) -> Self {
35 Self {
36 name: name.to_string(),
37 version: version.to_string(),
38 }
39 }
40}
41
42#[derive(PartialEq, Eq, Debug, Deserialize)]
43struct PackageMetadata {
47 version: String,
48}
49
50impl Display for Package {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 write!(f, "{} ({})", self.name, self.version)
53 }
54}
55
56#[derive(Deserialize, Debug)]
57struct TomlLock {
59 #[serde(rename = "package")]
60 packages: Vec<Package>,
61}
62
63impl TomlLock {
64 fn packages(self) -> Vec<Package> {
65 self.packages.into_iter().collect()
66 }
67}
68
69fn parse_toml_lock(contents: &str) -> Result<Vec<Package>> {
70 let lock: TomlLock = toml::from_str(contents)?;
71 Ok(lock.packages())
72}
73
74fn parse_lock(name: &str, contents: &str) -> Result<Vec<Package>> {
75 fn is_python_requirements(name: &str) -> bool {
76 name.contains("requirements") && name.ends_with(".txt")
77 }
78
79 match name {
80 "Cargo.lock" | "poetry.lock" | "uv.lock" => parse_toml_lock(contents),
81 "Gemfile.lock" => parse_gemfile_lock(contents),
82 "composer.lock" => parse_composer_lock(contents),
83 "go.sum" => parse_go_sum(contents),
84 "package-lock.json" => parse_npm_lock(contents),
85 "pubspec.lock" => parse_pubspec_lock(contents),
86 "shard.lock" => parse_shard_lock(contents),
87 "yarn.lock" => parse_yarn_lock(contents),
88 name if is_python_requirements(name) => parse_requirements_txt(contents),
89 _ => bail!("Unknown lock name: {name}"),
90 }
91}
92
93pub fn run() -> Result<()> {
94 let args: Vec<_> = std::env::args().collect();
101 if args.len() != 2 {
102 bail!("Expected exactly one arg");
103 }
104 let lock_path = &args[1];
105 let lock_path = PathBuf::from(lock_path);
106 let file_name = lock_path
107 .file_name()
108 .context("|| lock path should have a file name")?;
109 let lock_contents = std::fs::read_to_string(&lock_path).context("Could not read lock file")?;
110
111 if let Err(e) = print_lock(file_name, &lock_path, &lock_contents) {
114 println!("{lock_contents}");
115 eprintln!("Note: {e:#}");
116 }
117 Ok(())
118}
119
120fn print_lock(file_name: &OsStr, lock_path: &Path, contents: &str) -> Result<()> {
121 let file_name = file_name
122 .to_str()
123 .context("File name {file_name:?} should be UTF-8")?;
124 let packages = parse_lock(file_name, contents)
125 .context(format!("Could not parse {}", lock_path.display()))?;
126 for package in packages {
127 println!("{package}");
128 }
129 Ok(())
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn test_parse_toml_lock() {
138 let contents = r#"
139# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
140
141[[package]]
142name = "anyio"
143version = "4.8.0"
144description = "High level compatibility layer for multiple asynchronous event loop implementations"
145files = [
146]
147
148[package.dependencies]
149idna = ">=2.8"
150
151[package.extras]
152doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
153
154[[package]]
155name = "asgiref"
156version = "3.8.1"
157
158[metadata]
159lock-version = "2.1"
160python-versions = ">= 3.13"
161"#;
162 let packages = parse_toml_lock(contents).unwrap();
163 assert_eq!(
164 &packages,
165 &[
166 Package::new("anyio", "4.8.0"),
167 Package::new("asgiref", "3.8.1")
168 ]
169 );
170 }
171}