1use std::collections::{HashMap, HashSet};
5use std::fs;
6use std::path::Path;
7
8use chaste_types::{
9 package_name_str, Chastefile, ChastefileBuilder, Checksums, DependencyBuilder, DependencyKind,
10 InstallationBuilder, Integrity, ModulePath, PackageBuilder, PackageDerivation,
11 PackageDerivationMetaBuilder, PackageID, PackageName, PackagePatchBuilder, PackageSource,
12 SourceVersionSpecifier, SourceVersionSpecifierKind,
13};
14use nom::{
15 bytes::complete::tag,
16 combinator::{eof, map, rest},
17 multi::many0,
18 sequence::terminated,
19 Parser,
20};
21
22pub use crate::error::{Error, Result};
23use crate::types::LockPackageElement;
24
25#[cfg(feature = "fuzzing")]
26pub use crate::types::BunLock;
27
28#[cfg(not(feature = "fuzzing"))]
29use crate::types::BunLock;
30
31mod error;
32#[cfg(test)]
33mod tests;
34mod types;
35
36pub static LOCKFILE_NAME: &str = "bun.lock";
37
38type SourceKey<'a> = (&'a str, Vec<&'a str>);
39
40fn parse_package_key<'a>(input: &'a str) -> Result<(Option<SourceKey<'a>>, &'a str)> {
41 (
42 map(many0(terminated(package_name_str, tag("/"))), |pns| {
43 if !pns.is_empty() {
44 Some((
45 &input[..pns.iter().fold(0, |acc, pn| acc + pn.len() + 1) - 1],
46 pns,
47 ))
48 } else {
49 None
50 }
51 }),
52 terminated(package_name_str, eof),
53 )
54 .parse(input)
55 .map(|(_, r)| r)
56 .map_err(|_| Error::InvalidKey(input.to_string()))
57}
58
59fn parse_descriptor(input: &str) -> Result<(&str, &str)> {
60 (terminated(package_name_str, tag("@")), rest)
61 .parse(input)
62 .map(|(_, r)| r)
63 .map_err(|_| Error::InvalidKey(input.to_string()))
64}
65
66pub fn parse<P>(root_dir: P) -> Result<Chastefile>
67where
68 P: AsRef<Path>,
69{
70 let bun_lock_contents = fs::read_to_string(root_dir.as_ref().join(LOCKFILE_NAME))?;
71 let bun_lock: BunLock = json5::from_str(&bun_lock_contents)?;
72 parse_contents(bun_lock)
73}
74
75#[cfg(feature = "fuzzing")]
76pub fn parse_lock(bun_lock: BunLock) -> Result<Chastefile> {
77 parse_contents(bun_lock)
78}
79
80fn parse_contents(bun_lock: BunLock) -> Result<Chastefile> {
81 if !matches!(bun_lock.lockfile_version, (0..=1)) {
82 return Err(Error::UnknownLockfileVersion(bun_lock.lockfile_version));
83 }
84
85 let mut chastefile = ChastefileBuilder::new();
86
87 let mut ws_location_to_pid: HashMap<&str, PackageID> =
88 HashMap::with_capacity(bun_lock.workspaces.len());
89 for (ws_location, ws_member) in &bun_lock.workspaces {
90 let ws_path = ModulePath::new(ws_location.to_string())?;
91 let pkg_builder = PackageBuilder::new(
92 ws_member
93 .name
94 .as_ref()
95 .map(|n| PackageName::new(n.to_string()))
96 .transpose()?,
97 ws_member.version.as_ref().map(|n| n.to_string()),
98 );
99 let pid = chastefile.add_package(pkg_builder.build()?)?;
100 chastefile.add_package_installation(InstallationBuilder::new(pid, ws_path).build()?);
101 if ws_location.is_empty() {
102 chastefile.set_root_package_id(pid)?;
103 } else {
104 chastefile.set_as_workspace_member(pid)?;
105 }
106 ws_location_to_pid.insert(ws_location, pid);
107 }
108
109 let mut descript_to_pid: HashMap<&str, PackageID> =
110 HashMap::with_capacity(bun_lock.packages.len());
111 let mut presolved_unhoistable: HashMap<(&str, &str), PackageID> = HashMap::new();
112 let mut aliased_pids: HashSet<PackageID> = HashSet::new();
113 for (lock_key, lock_pkg) in &bun_lock.packages {
114 let (source, installation_package_name) = parse_package_key(lock_key)?;
115 let descriptor = match &lock_pkg[..] {
116 [LockPackageElement::String(d), ..] => d.as_ref(),
117 _ => return Err(Error::InvalidVariant(lock_key.to_string())),
118 };
119 if let Some(pid) = descript_to_pid.get(descriptor) {
122 if let Some((source_key, _)) = source {
123 presolved_unhoistable.insert((source_key, installation_package_name), *pid);
124 }
125 } else {
126 let (package_name, sv_marker) = parse_descriptor(descriptor)?;
127 let pid = if let Some(pid) = sv_marker
128 .strip_prefix("workspace:")
129 .and_then(|l| ws_location_to_pid.get(l))
130 {
131 *pid
132 } else {
133 let sm_svs = SourceVersionSpecifier::new(sv_marker.to_string())?;
134 let patch_path = bun_lock.patched_dependencies.get(descriptor);
135 let pkg_name = PackageName::new(package_name.to_string())?;
136 let mut patched_pkg_builder = if patch_path.is_some() {
137 Some(PackageBuilder::new(Some(pkg_name.clone()), None))
138 } else {
139 None
140 };
141 let mut pkg_builder = PackageBuilder::new(Some(pkg_name), None);
142 match (&lock_pkg[..], sm_svs.kind()) {
143 (
144 [LockPackageElement::String(_descriptor), LockPackageElement::String(_registry_url), LockPackageElement::Relations(_relations), LockPackageElement::String(integrity)],
145 SourceVersionSpecifierKind::Npm,
146 ) => {
147 if let Some(patched) = &mut patched_pkg_builder {
148 patched.version(Some(sv_marker.to_string()));
149 }
150 pkg_builder.version(Some(sv_marker.to_string()));
151 let integrity = integrity.parse::<Integrity>()?;
152 if !integrity.hashes.is_empty() {
153 pkg_builder.checksums(Checksums::Tarball(integrity));
154 }
155 pkg_builder.source(PackageSource::Npm);
156 }
157 (_, SourceVersionSpecifierKind::TarballURL) => {
158 pkg_builder.source(PackageSource::TarballURL {
159 url: sv_marker.to_string(),
160 });
161 }
162 (_, SourceVersionSpecifierKind::Git | SourceVersionSpecifierKind::GitHub) => {
163 if !sm_svs.is_github() {
164 pkg_builder.source(PackageSource::Git {
165 url: sv_marker.to_string(),
166 });
167 }
168 }
169 (_, _) => return Err(Error::VariantMarkerMismatch(lock_key.to_string())),
170 }
171 let p = if let Some(mut patched) = patched_pkg_builder {
172 let og_p = chastefile.add_package(pkg_builder.build()?)?;
173 let patch =
174 PackagePatchBuilder::new(patch_path.unwrap().to_string()).build()?;
175 patched.derived(
176 PackageDerivationMetaBuilder::new(PackageDerivation::Patch(patch), og_p)
177 .build()?,
178 );
179 chastefile.add_package(patched.build()?)?
180 } else {
181 chastefile.add_package(pkg_builder.build()?)?
182 };
183 if installation_package_name != package_name {
184 aliased_pids.insert(p);
185 }
186 p
187 };
188 descript_to_pid.insert(descriptor, pid);
189 if let Some((source_key, _)) = source {
190 presolved_unhoistable.insert((source_key, installation_package_name), pid);
191 }
192 let module_path = ModulePath::new(if let Some((_, parent_modules)) = source {
193 let expected_len = lock_key.len() + (parent_modules.len() * 13) + 13;
194 let mut mp = String::with_capacity(expected_len);
195 for pm in parent_modules {
196 mp += "node_modules/";
197 mp += pm;
198 mp += "/";
199 }
200 mp += "node_modules/";
201 mp += installation_package_name;
202 debug_assert_eq!(mp.len(), expected_len);
203 mp
204 } else {
205 format!("node_modules/{installation_package_name}")
206 })?;
207 chastefile
208 .add_package_installation(InstallationBuilder::new(pid, module_path).build()?);
209 }
210 }
211 for (lock_key, lock_pkg) in &bun_lock.packages {
212 let descriptor = match &lock_pkg[..] {
213 [LockPackageElement::String(d), ..] => d.as_ref(),
214 _ => unreachable!(),
216 };
217 let mut relations = None;
218 for idx in 0..lock_pkg.len() {
219 if let LockPackageElement::Relations(rels) = &lock_pkg[idx] {
220 relations = Some(rels);
221 if lock_pkg[idx + 1..]
222 .iter()
223 .any(|e| matches!(e, LockPackageElement::Relations(_)))
224 {
225 return Err(Error::InvalidVariant(lock_key.to_string()));
226 }
227 break;
228 }
229 }
230 let pid = *descript_to_pid.get(descriptor).unwrap();
231 if let Some(relations) = relations {
232 for (deps, kind_) in [
233 (&relations.dependencies, DependencyKind::Dependency),
234 (&relations.dev_dependencies, DependencyKind::DevDependency),
235 (&relations.peer_dependencies, DependencyKind::PeerDependency),
236 (
237 &relations.optional_dependencies,
238 DependencyKind::OptionalDependency,
239 ),
240 ] {
241 for (dep_name, dep_svs) in deps {
242 let kind = match kind_ {
243 DependencyKind::PeerDependency
244 if relations.optional_peers.contains(dep_name) =>
245 {
246 DependencyKind::OptionalPeerDependency
247 }
248 k => k,
249 };
250 match presolved_unhoistable
251 .get(&(lock_key, dep_name))
252 .or_else(|| {
253 bun_lock.packages.get(dep_name).and_then(|p| {
254 let descriptor = match &p[..] {
255 [LockPackageElement::String(d), ..] => d.as_ref(),
256 _ => unreachable!(),
258 };
259 descript_to_pid.get(descriptor)
260 })
261 }) {
262 Some(dep_pid) => {
263 let mut dep = DependencyBuilder::new(kind, pid, *dep_pid);
264 dep.svs(SourceVersionSpecifier::new(dep_svs.to_string())?);
265 chastefile.add_dependency(dep.build());
266 }
267 None if kind.is_optional() => {}
268 None => {
269 return Err(Error::DependencyNotFound(format!("{dep_name}@{dep_svs}")))
270 }
271 };
272 }
273 }
274 }
275 }
276 for (ws_location, ws_member) in &bun_lock.workspaces {
277 let relations = &ws_member.relations;
278 let pid = *ws_location_to_pid.get(ws_location.as_ref()).unwrap();
279
280 for (deps, kind_) in [
281 (&relations.dependencies, DependencyKind::Dependency),
282 (&relations.dev_dependencies, DependencyKind::DevDependency),
283 (&relations.peer_dependencies, DependencyKind::PeerDependency),
284 (
285 &relations.optional_dependencies,
286 DependencyKind::OptionalDependency,
287 ),
288 ] {
289 for (dep_name, dep_svs) in deps {
290 let kind = match kind_ {
291 DependencyKind::PeerDependency
292 if relations.optional_peers.contains(dep_name) =>
293 {
294 DependencyKind::OptionalPeerDependency
295 }
296 k => k,
297 };
298 match bun_lock.packages.get(dep_name).and_then(|p| {
299 let descriptor = match &p[..] {
300 [LockPackageElement::String(d), ..] => d.as_ref(),
301 _ => unreachable!(),
303 };
304 descript_to_pid.get(descriptor)
305 }) {
306 Some(dep_pid) => {
307 let mut dep = DependencyBuilder::new(kind, pid, *dep_pid);
308 dep.svs(SourceVersionSpecifier::new(dep_svs.to_string())?);
309 if aliased_pids.contains(dep_pid) {
310 dep.alias_name(PackageName::new(dep_name.to_string())?);
311 }
312 chastefile.add_dependency(dep.build());
313 }
314 None if kind.is_optional() => {}
315 None => return Err(Error::DependencyNotFound(format!("{dep_name}@{dep_svs}"))),
316 };
317 }
318 }
319 }
320
321 chastefile.build().map_err(Error::ChasteError)
322}
323
324#[cfg(test)]
325mod unit_tests {
326 use crate::{parse_package_key, Result};
327
328 #[test]
329 fn test_parse_package_key() -> Result<()> {
330 assert_eq!(parse_package_key("ltx")?, (None, "ltx"));
331 assert_eq!(parse_package_key("@types/node")?, (None, "@types/node"));
332 assert_eq!(
333 parse_package_key("@xmpp/xml/ltx")?,
334 (Some(("@xmpp/xml", vec!["@xmpp/xml"])), "ltx")
335 );
336 assert_eq!(
337 parse_package_key("socket.io/debug/ms")?,
338 (Some(("socket.io/debug", vec!["socket.io", "debug"])), "ms")
339 );
340 Ok(())
341 }
342}