Skip to main content

chaste_bun/
lib.rs

1// SPDX-FileCopyrightText: 2025 The Chaste Authors
2// SPDX-License-Identifier: Apache-2.0 OR BSD-2-Clause
3
4use 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        // Packages repeat, so we dedup them by the descriptor.
120        // But we still want to reverse search them by key.
121        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            // This should have thrown an InvalidVariant earlier
215            _ => 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                                    // This should have thrown InvalidVariant earlier
257                                    _ => 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                        // This should have thrown InvalidVariant earlier
302                        _ => 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}