Skip to main content

provenant/parsers/
bun_lockb.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashMap;
5use std::path::Path;
6
7use crate::parser_warn as warn;
8use base64::Engine;
9use serde_json::Value as JsonValue;
10
11use crate::models::{
12    DatasourceId, Dependency, PackageData, PackageType, ResolvedPackage, Sha512Digest,
13};
14use crate::parsers::utils::{
15    MAX_ITERATION_COUNT, MAX_RECURSION_DEPTH, RecursionGuard, npm_purl, parse_sri, truncate_field,
16};
17
18use super::PackageParser;
19
20pub struct BunLockbParser;
21
22const HEADER_BYTES: &[u8] = b"#!/usr/bin/env bun\nbun-lockfile-format-v0\n";
23const SUPPORTED_FORMAT_VERSION: u32 = 2;
24const FIELD_COUNT_WITHOUT_SCRIPTS: usize = 7;
25const FIELD_COUNT_WITH_SCRIPTS: usize = 8;
26const PACKAGE_FIELD_LENGTHS: [usize; 8] = [8, 8, 64, 8, 8, 88, 20, 48];
27const DEPENDENCY_ENTRY_SIZE: usize = 26;
28const MAX_MANIFEST_SIZE: u64 = 100 * 1024 * 1024;
29
30#[derive(Clone, Copy)]
31struct SliceRef {
32    off: usize,
33    len: usize,
34}
35
36#[derive(Clone)]
37struct BunLockbPackage {
38    name_ref: [u8; 8],
39    name: String,
40    resolution_raw: [u8; 64],
41    resolution: BunLockbResolution,
42    dependencies: SliceRef,
43    resolutions: SliceRef,
44    integrity: Option<String>,
45}
46
47#[derive(Clone)]
48struct BunLockbResolution {
49    version: Option<String>,
50    resolved: Option<String>,
51}
52
53#[derive(Clone)]
54struct BunLockbDependencyEntry {
55    name: String,
56    literal: String,
57    behavior: u8,
58}
59
60struct BunLockbBuffers<'a> {
61    resolutions: &'a [u8],
62    dependencies: &'a [u8],
63    string_bytes: &'a [u8],
64}
65
66struct LockbCursor<'a> {
67    bytes: &'a [u8],
68    pos: usize,
69}
70
71impl PackageParser for BunLockbParser {
72    const PACKAGE_TYPE: PackageType = PackageType::Npm;
73
74    fn is_match(path: &Path) -> bool {
75        path.file_name()
76            .and_then(|name| name.to_str())
77            .is_some_and(|name| name == "bun.lockb")
78            && !path.with_file_name("bun.lock").exists()
79    }
80
81    fn extract_packages(path: &Path) -> Vec<PackageData> {
82        let file_size = match std::fs::metadata(path) {
83            Ok(meta) => meta.len(),
84            Err(e) => {
85                warn!("Failed to stat bun.lockb at {:?}: {}", path, e);
86                return vec![default_package_data()];
87            }
88        };
89        if file_size > MAX_MANIFEST_SIZE {
90            warn!(
91                "bun.lockb at {:?} is too large ({} bytes, max {})",
92                path, file_size, MAX_MANIFEST_SIZE
93            );
94            return vec![default_package_data()];
95        }
96
97        let bytes = match std::fs::read(path) {
98            Ok(bytes) => bytes,
99            Err(e) => {
100                warn!("Failed to read bun.lockb at {:?}: {}", path, e);
101                return vec![default_package_data()];
102            }
103        };
104
105        match parse_bun_lockb(&bytes) {
106            Ok(package_data) => vec![package_data],
107            Err(e) => {
108                warn!("Failed to parse bun.lockb at {:?}: {}", path, e);
109                vec![default_package_data()]
110            }
111        }
112    }
113}
114
115fn default_package_data() -> PackageData {
116    PackageData {
117        package_type: Some(BunLockbParser::PACKAGE_TYPE),
118        primary_language: Some("JavaScript".to_string()),
119        datasource_id: Some(DatasourceId::BunLockb),
120        extra_data: Some(HashMap::new()),
121        ..Default::default()
122    }
123}
124
125pub(crate) fn parse_bun_lockb(bytes: &[u8]) -> Result<PackageData, String> {
126    let mut cursor = LockbCursor::new(bytes);
127    cursor.expect_bytes(HEADER_BYTES)?;
128
129    let format_version = cursor.read_u32()?;
130    if format_version != SUPPORTED_FORMAT_VERSION {
131        return Err(format!(
132            "Unsupported bun.lockb format version {} (supported: {})",
133            format_version, SUPPORTED_FORMAT_VERSION
134        ));
135    }
136
137    let meta_hash = cursor.read_bytes(32)?;
138    let total_buffer_size = cursor.read_u64()? as usize;
139    if total_buffer_size > bytes.len() {
140        return Err("Lockfile is missing data".to_string());
141    }
142
143    let list_len = cursor.read_u64()? as usize;
144    let input_alignment = cursor.read_u64()?;
145    if input_alignment != 8 {
146        return Err(format!(
147            "Unexpected bun.lockb package alignment {}",
148            input_alignment
149        ));
150    }
151
152    let field_count = cursor.read_u64()? as usize;
153    if field_count != FIELD_COUNT_WITHOUT_SCRIPTS && field_count != FIELD_COUNT_WITH_SCRIPTS {
154        return Err(format!(
155            "Unexpected bun.lockb package field count {} (supported: {} or {})",
156            field_count, FIELD_COUNT_WITHOUT_SCRIPTS, FIELD_COUNT_WITH_SCRIPTS
157        ));
158    }
159
160    let packages_begin = cursor.read_u64()? as usize;
161    let packages_end = cursor.read_u64()? as usize;
162    if packages_begin > total_buffer_size
163        || packages_end > total_buffer_size
164        || packages_begin > packages_end
165    {
166        return Err("Invalid bun.lockb package section bounds".to_string());
167    }
168
169    let mut packages = parse_packages(bytes, list_len, field_count, packages_begin, packages_end)?;
170    cursor.pos = packages_end;
171    let buffers = parse_buffers(bytes, &mut cursor, total_buffer_size)?;
172    materialize_packages(&mut packages, buffers.string_bytes)?;
173
174    build_package_data_from_lockb(format_version, meta_hash, &packages, &buffers)
175}
176
177fn parse_packages(
178    bytes: &[u8],
179    list_len: usize,
180    field_count: usize,
181    packages_begin: usize,
182    packages_end: usize,
183) -> Result<Vec<BunLockbPackage>, String> {
184    if list_len > MAX_ITERATION_COUNT {
185        return Err(format!(
186            "bun.lockb package count {} exceeds maximum {}",
187            list_len, MAX_ITERATION_COUNT
188        ));
189    }
190
191    let mut packages = vec![
192        BunLockbPackage {
193            name_ref: [0; 8],
194            name: String::new(),
195            resolution_raw: [0; 64],
196            resolution: BunLockbResolution {
197                version: None,
198                resolved: None,
199            },
200            dependencies: SliceRef { off: 0, len: 0 },
201            resolutions: SliceRef { off: 0, len: 0 },
202            integrity: None,
203        };
204        list_len
205    ];
206
207    let package_region = bytes
208        .get(packages_begin..packages_end)
209        .ok_or_else(|| "Invalid bun.lockb package region".to_string())?;
210
211    let expected_size: usize =
212        PACKAGE_FIELD_LENGTHS[..field_count].iter().sum::<usize>() * list_len;
213    if package_region.len() < expected_size {
214        return Err("bun.lockb package region is truncated".to_string());
215    }
216
217    let mut field_offset = 0usize;
218
219    for package in &mut packages {
220        package
221            .name_ref
222            .copy_from_slice(&package_region[field_offset..field_offset + 8]);
223        field_offset += 8;
224    }
225
226    field_offset += 8 * list_len;
227
228    for package in &mut packages {
229        package
230            .resolution_raw
231            .copy_from_slice(&package_region[field_offset..field_offset + 64]);
232        field_offset += 64;
233    }
234
235    for package in &mut packages {
236        package.dependencies = parse_slice_ref(&package_region[field_offset..field_offset + 8])?;
237        field_offset += 8;
238    }
239
240    for package in &mut packages {
241        package.resolutions = parse_slice_ref(&package_region[field_offset..field_offset + 8])?;
242        field_offset += 8;
243    }
244
245    for package in &mut packages {
246        package.integrity = parse_integrity(&package_region[field_offset + 20..field_offset + 85]);
247        field_offset += 88;
248    }
249
250    field_offset += 20 * list_len;
251    if field_count == FIELD_COUNT_WITH_SCRIPTS {
252        field_offset += 48 * list_len;
253    }
254
255    if field_offset != expected_size {
256        return Err("bun.lockb package region layout is malformed".to_string());
257    }
258
259    Ok(packages)
260}
261
262fn materialize_packages(
263    packages: &mut [BunLockbPackage],
264    string_bytes: &[u8],
265) -> Result<(), String> {
266    for package in packages {
267        package.name = decode_bun_string(&package.name_ref, string_bytes)?;
268        package.resolution = parse_resolution(&package.resolution_raw, string_bytes)?;
269    }
270    Ok(())
271}
272
273fn parse_buffers<'a>(
274    bytes: &'a [u8],
275    cursor: &mut LockbCursor<'a>,
276    total_buffer_size: usize,
277) -> Result<BunLockbBuffers<'a>, String> {
278    let _trees = parse_buffer_range(bytes, cursor, total_buffer_size)?;
279    let _hoisted_dependencies = parse_buffer_range(bytes, cursor, total_buffer_size)?;
280    let resolutions = parse_buffer_range(bytes, cursor, total_buffer_size)?;
281    let dependencies = parse_buffer_range(bytes, cursor, total_buffer_size)?;
282    let _extern_strings = parse_buffer_range(bytes, cursor, total_buffer_size)?;
283    let string_bytes = parse_buffer_range(bytes, cursor, total_buffer_size)?;
284
285    Ok(BunLockbBuffers {
286        resolutions,
287        dependencies,
288        string_bytes,
289    })
290}
291
292fn parse_buffer_range<'a>(
293    bytes: &'a [u8],
294    cursor: &mut LockbCursor<'a>,
295    total_buffer_size: usize,
296) -> Result<&'a [u8], String> {
297    let start = cursor.read_u64()? as usize;
298    let end = cursor.read_u64()? as usize;
299    if start > total_buffer_size || end > total_buffer_size || start > end {
300        return Err("Invalid bun.lockb buffer range".to_string());
301    }
302    cursor.pos = start;
303    let slice = cursor.read_bytes(end - start)?;
304    cursor.pos = end;
305    bytes
306        .get(start..end)
307        .or(Some(slice))
308        .ok_or_else(|| "Invalid bun.lockb buffer slice".to_string())
309}
310
311fn build_package_data_from_lockb(
312    format_version: u32,
313    meta_hash: &[u8],
314    packages: &[BunLockbPackage],
315    buffers: &BunLockbBuffers<'_>,
316) -> Result<PackageData, String> {
317    let root_package = packages
318        .first()
319        .ok_or_else(|| "bun.lockb contains no packages".to_string())?;
320
321    let mut package_data = default_package_data();
322    package_data.name = Some(truncate_field(root_package.name.clone()));
323    package_data.purl = npm_purl(&root_package.name, None);
324
325    let extra_data = package_data.extra_data.get_or_insert_with(HashMap::new);
326    extra_data.insert(
327        "lockfileVersion".to_string(),
328        JsonValue::from(i64::from(format_version)),
329    );
330    extra_data.insert(
331        "meta_hash".to_string(),
332        JsonValue::from(encode_hex(meta_hash)),
333    );
334
335    let dependency_entries = parse_dependency_entries(buffers.dependencies, buffers.string_bytes)?;
336    let resolution_ids = parse_resolution_ids(buffers.resolutions)?;
337
338    package_data.dependencies = build_dependencies_for_package(
339        root_package,
340        packages,
341        &dependency_entries,
342        &resolution_ids,
343        buffers.string_bytes,
344        true,
345        &mut RecursionGuard::<usize>::new(),
346    )?;
347
348    Ok(package_data)
349}
350
351fn parse_dependency_entries(
352    bytes: &[u8],
353    string_bytes: &[u8],
354) -> Result<Vec<BunLockbDependencyEntry>, String> {
355    if !bytes.len().is_multiple_of(DEPENDENCY_ENTRY_SIZE) {
356        return Err("bun.lockb dependency buffer is malformed".to_string());
357    }
358
359    bytes
360        .chunks_exact(DEPENDENCY_ENTRY_SIZE)
361        .take(MAX_ITERATION_COUNT)
362        .map(|entry| {
363            Ok(BunLockbDependencyEntry {
364                name: decode_bun_string(&entry[0..8], string_bytes)?,
365                behavior: entry[16],
366                literal: decode_bun_string(&entry[18..26], string_bytes)?,
367            })
368        })
369        .collect()
370}
371
372fn parse_resolution_ids(bytes: &[u8]) -> Result<Vec<u32>, String> {
373    if !bytes.len().is_multiple_of(4) {
374        return Err("bun.lockb resolution buffer is malformed".to_string());
375    }
376
377    bytes
378        .chunks_exact(4)
379        .take(MAX_ITERATION_COUNT)
380        .map(|chunk| {
381            let arr: [u8; 4] = chunk
382                .try_into()
383                .map_err(|_| "Invalid bun.lockb resolution entry".to_string())?;
384            Ok(u32::from_le_bytes(arr))
385        })
386        .collect()
387}
388
389fn build_dependencies_for_package(
390    package: &BunLockbPackage,
391    packages: &[BunLockbPackage],
392    dependency_entries: &[BunLockbDependencyEntry],
393    resolution_ids: &[u32],
394    string_bytes: &[u8],
395    is_direct: bool,
396    guard: &mut RecursionGuard<usize>,
397) -> Result<Vec<Dependency>, String> {
398    if guard.exceeded() {
399        warn!(
400            "bun.lockb build_dependencies_for_package exceeded MAX_RECURSION_DEPTH ({})",
401            MAX_RECURSION_DEPTH
402        );
403        return Ok(vec![]);
404    }
405
406    let dep_slice = dependency_entries
407        .get(package.dependencies.off..package.dependencies.off + package.dependencies.len)
408        .ok_or_else(|| "bun.lockb dependency slice is out of bounds".to_string())?;
409    let res_slice = resolution_ids
410        .get(package.resolutions.off..package.resolutions.off + package.resolutions.len)
411        .ok_or_else(|| "bun.lockb resolution slice is out of bounds".to_string())?;
412
413    dep_slice
414        .iter()
415        .zip(res_slice.iter())
416        .take(MAX_ITERATION_COUNT)
417        .map(|(entry, package_id)| {
418            let manifest = behavior_to_manifest(entry.behavior);
419            let resolved_package = if (*package_id as usize) < packages.len() {
420                let pkg_idx = *package_id as usize;
421                if guard.enter(pkg_idx) {
422                    warn!(
423                        "bun.lockb circular dependency detected for package index {}",
424                        pkg_idx
425                    );
426                    None
427                } else {
428                    let resolved = &packages[pkg_idx];
429                    let result = build_resolved_package(
430                        resolved,
431                        packages,
432                        dependency_entries,
433                        resolution_ids,
434                        string_bytes,
435                        guard,
436                    )?;
437                    guard.leave(pkg_idx);
438                    Some(Box::new(result))
439                }
440            } else {
441                None
442            };
443
444            let version = resolved_package
445                .as_ref()
446                .and_then(|pkg| (!pkg.version.is_empty()).then_some(pkg.version.as_str()));
447
448            Ok(Dependency {
449                purl: npm_purl(&truncate_field(entry.name.clone()), version),
450                extracted_requirement: Some(truncate_field(entry.literal.clone())),
451                scope: Some(manifest.scope.to_string()),
452                is_runtime: Some(manifest.is_runtime),
453                is_optional: Some(manifest.is_optional),
454                is_pinned: version.map(|_| true).or(Some(false)),
455                is_direct: Some(is_direct),
456                resolved_package,
457                extra_data: None,
458            })
459        })
460        .collect()
461}
462
463fn build_resolved_package(
464    package: &BunLockbPackage,
465    packages: &[BunLockbPackage],
466    dependency_entries: &[BunLockbDependencyEntry],
467    resolution_ids: &[u32],
468    string_bytes: &[u8],
469    guard: &mut RecursionGuard<usize>,
470) -> Result<ResolvedPackage, String> {
471    if guard.exceeded() {
472        warn!(
473            "bun.lockb build_resolved_package exceeded MAX_RECURSION_DEPTH ({})",
474            MAX_RECURSION_DEPTH
475        );
476        let (namespace, name) = split_namespace_name(&package.name);
477        return Ok(ResolvedPackage {
478            primary_language: Some("JavaScript".to_string()),
479            download_url: package
480                .resolution
481                .resolved
482                .as_ref()
483                .map(|s| truncate_field(s.clone())),
484            sha1: None,
485            sha256: None,
486            sha512: package
487                .integrity
488                .as_ref()
489                .and_then(|s| {
490                    parse_sri(s).and_then(|(alg, hash)| (alg == "sha512").then_some(hash))
491                })
492                .and_then(|h| Sha512Digest::from_hex(&h).ok()),
493            md5: None,
494            is_virtual: true,
495            extra_data: None,
496            dependencies: vec![],
497            repository_homepage_url: None,
498            repository_download_url: None,
499            api_data_url: None,
500            datasource_id: Some(DatasourceId::BunLockb),
501            purl: None,
502            ..ResolvedPackage::new(
503                PackageType::Npm,
504                namespace.map(truncate_field).unwrap_or_default(),
505                name.map(truncate_field)
506                    .unwrap_or_else(|| truncate_field(package.name.clone())),
507                truncate_field(package.resolution.version.clone().unwrap_or_default()),
508            )
509        });
510    }
511
512    let (namespace, name) = split_namespace_name(&package.name);
513
514    Ok(ResolvedPackage {
515        primary_language: Some("JavaScript".to_string()),
516        download_url: package
517            .resolution
518            .resolved
519            .as_ref()
520            .map(|s| truncate_field(s.clone())),
521        sha1: None,
522        sha256: None,
523        sha512: package
524            .integrity
525            .as_ref()
526            .and_then(|s| parse_sri(s).and_then(|(alg, hash)| (alg == "sha512").then_some(hash)))
527            .and_then(|h| Sha512Digest::from_hex(&h).ok()),
528        md5: None,
529        is_virtual: true,
530        extra_data: None,
531        dependencies: build_dependencies_for_package(
532            package,
533            packages,
534            dependency_entries,
535            resolution_ids,
536            string_bytes,
537            false,
538            guard,
539        )?,
540        repository_homepage_url: None,
541        repository_download_url: None,
542        api_data_url: None,
543        datasource_id: Some(DatasourceId::BunLockb),
544        purl: None,
545        ..ResolvedPackage::new(
546            PackageType::Npm,
547            namespace.map(truncate_field).unwrap_or_default(),
548            name.map(truncate_field)
549                .unwrap_or_else(|| truncate_field(package.name.clone())),
550            truncate_field(package.resolution.version.clone().unwrap_or_default()),
551        )
552    })
553}
554
555fn parse_slice_ref(bytes: &[u8]) -> Result<SliceRef, String> {
556    if bytes.len() != 8 {
557        return Err("Invalid bun.lockb slice length".to_string());
558    }
559    let off = u32::from_le_bytes(
560        bytes[0..4]
561            .try_into()
562            .map_err(|_| "Invalid bun.lockb slice offset".to_string())?,
563    ) as usize;
564    let len = u32::from_le_bytes(
565        bytes[4..8]
566            .try_into()
567            .map_err(|_| "Invalid bun.lockb slice length".to_string())?,
568    ) as usize;
569    Ok(SliceRef { off, len })
570}
571
572fn parse_resolution(bytes: &[u8], string_bytes: &[u8]) -> Result<BunLockbResolution, String> {
573    if bytes.len() != 64 {
574        return Err("Invalid bun.lockb resolution length".to_string());
575    }
576
577    let tag = bytes[0];
578    match tag {
579        1 => Ok(BunLockbResolution {
580            version: None,
581            resolved: Some(String::new()).filter(|s| !s.is_empty()),
582        }),
583        2 => {
584            let resolved = decode_bun_string(&bytes[8..16], string_bytes)?;
585            let major = u32::from_le_bytes(
586                bytes[16..20]
587                    .try_into()
588                    .map_err(|_| "Invalid bun.lockb version major".to_string())?,
589            );
590            let minor = u32::from_le_bytes(
591                bytes[20..24]
592                    .try_into()
593                    .map_err(|_| "Invalid bun.lockb version minor".to_string())?,
594            );
595            let patch = u32::from_le_bytes(
596                bytes[24..28]
597                    .try_into()
598                    .map_err(|_| "Invalid bun.lockb version patch".to_string())?,
599            );
600            let tag_suffix = decode_version_suffix(&bytes[32..64], string_bytes)?;
601            let version = if let Some(suffix) = tag_suffix {
602                format!("{}.{}.{}{}", major, minor, patch, suffix)
603            } else {
604                format!("{}.{}.{}", major, minor, patch)
605            };
606
607            Ok(BunLockbResolution {
608                version: Some(truncate_field(version)),
609                resolved: (!resolved.is_empty()).then_some(truncate_field(resolved)),
610            })
611        }
612        72 => {
613            let workspace = decode_bun_string(&bytes[8..16], string_bytes)?;
614            Ok(BunLockbResolution {
615                version: None,
616                resolved: Some(truncate_field(format!("workspace:{}", workspace))),
617            })
618        }
619        4 | 8 | 16 | 24 | 32 | 64 | 80 | 100 => {
620            let resolved = decode_bun_string(&bytes[8..16], string_bytes)?;
621            Ok(BunLockbResolution {
622                version: None,
623                resolved: (!resolved.is_empty()).then_some(truncate_field(resolved)),
624            })
625        }
626        _ => Err(format!("Unsupported bun.lockb resolution tag {}", tag)),
627    }
628}
629
630fn decode_version_suffix(bytes: &[u8], string_bytes: &[u8]) -> Result<Option<String>, String> {
631    if bytes.len() != 32 {
632        return Err("Invalid bun.lockb version tag length".to_string());
633    }
634    let pre = decode_bun_string(&bytes[0..8], string_bytes)?;
635    let build = decode_bun_string(&bytes[16..24], string_bytes)?;
636
637    let mut suffix = String::new();
638    if !pre.is_empty() {
639        suffix.push('-');
640        suffix.push_str(&pre);
641    }
642    if !build.is_empty() {
643        suffix.push('+');
644        suffix.push_str(&build);
645    }
646
647    Ok((!suffix.is_empty()).then_some(suffix))
648}
649
650fn decode_bun_string(bytes: &[u8], string_bytes: &[u8]) -> Result<String, String> {
651    if bytes.len() != 8 {
652        return Err("Invalid bun.lockb string width".to_string());
653    }
654
655    if bytes[7] & 0x80 == 0 {
656        let end = bytes.iter().position(|b| *b == 0).unwrap_or(bytes.len());
657        let slice = &bytes[..end];
658        return if let Ok(s) = std::str::from_utf8(slice) {
659            Ok(s.to_string())
660        } else {
661            warn!("Invalid bun.lockb UTF-8 in string, using lossy conversion");
662            Ok(String::from_utf8_lossy(slice).into_owned())
663        };
664    }
665
666    let off = u32::from_le_bytes(
667        bytes[0..4]
668            .try_into()
669            .map_err(|_| "Invalid bun.lockb string offset".to_string())?,
670    ) as usize;
671    let len_raw = u32::from_le_bytes(
672        bytes[4..8]
673            .try_into()
674            .map_err(|_| "Invalid bun.lockb string length".to_string())?,
675    );
676    let len = (len_raw & 0x7fff_ffff) as usize;
677    let slice = string_bytes
678        .get(off..off + len)
679        .ok_or_else(|| "bun.lockb string offset out of bounds".to_string())?;
680    if let Ok(s) = std::str::from_utf8(slice) {
681        Ok(s.to_string())
682    } else {
683        warn!("Invalid bun.lockb UTF-8 in string, using lossy conversion");
684        Ok(String::from_utf8_lossy(slice).into_owned())
685    }
686}
687
688fn parse_integrity(bytes: &[u8]) -> Option<String> {
689    if bytes.is_empty() {
690        return None;
691    }
692
693    let algorithm = match bytes[0] {
694        1 => "sha1",
695        2 => "sha256",
696        3 => "sha384",
697        4 => "sha512",
698        _ => return None,
699    };
700
701    Some(format!(
702        "{}-{}",
703        algorithm,
704        base64::engine::general_purpose::STANDARD.encode(&bytes[1..])
705    ))
706}
707
708fn encode_hex(bytes: &[u8]) -> String {
709    const HEX: &[u8; 16] = b"0123456789abcdef";
710    let mut out = String::with_capacity(bytes.len() * 2);
711    for byte in bytes {
712        out.push(HEX[(byte >> 4) as usize] as char);
713        out.push(HEX[(byte & 0x0f) as usize] as char);
714    }
715    out
716}
717
718fn split_namespace_name(full_name: &str) -> (Option<String>, Option<String>) {
719    if full_name.starts_with('@') {
720        let mut parts = full_name.splitn(2, '/');
721        let namespace = parts.next().map(ToOwned::to_owned);
722        let name = parts.next().map(ToOwned::to_owned);
723        (namespace, name)
724    } else {
725        (Some(String::new()), Some(full_name.to_string()))
726    }
727}
728
729struct ManifestBehavior {
730    scope: &'static str,
731    is_runtime: bool,
732    is_optional: bool,
733}
734
735fn behavior_to_manifest(behavior: u8) -> ManifestBehavior {
736    const NORMAL: u8 = 0b10;
737    const OPTIONAL: u8 = 0b100;
738    const DEV: u8 = 0b1000;
739    const PEER: u8 = 0b1_0000;
740    const WORKSPACE: u8 = 0b10_0000;
741
742    if behavior & WORKSPACE != 0 {
743        return ManifestBehavior {
744            scope: "workspaces",
745            is_runtime: false,
746            is_optional: false,
747        };
748    }
749    if behavior & DEV != 0 {
750        return ManifestBehavior {
751            scope: "devDependencies",
752            is_runtime: false,
753            is_optional: true,
754        };
755    }
756    if behavior & PEER != 0 && behavior & OPTIONAL != 0 {
757        return ManifestBehavior {
758            scope: "peerDependencies",
759            is_runtime: true,
760            is_optional: true,
761        };
762    }
763    if behavior & PEER != 0 {
764        return ManifestBehavior {
765            scope: "peerDependencies",
766            is_runtime: true,
767            is_optional: false,
768        };
769    }
770    if behavior & OPTIONAL != 0 {
771        return ManifestBehavior {
772            scope: "optionalDependencies",
773            is_runtime: true,
774            is_optional: true,
775        };
776    }
777    if behavior & NORMAL != 0 {
778        return ManifestBehavior {
779            scope: "dependencies",
780            is_runtime: true,
781            is_optional: false,
782        };
783    }
784
785    ManifestBehavior {
786        scope: "dependencies",
787        is_runtime: true,
788        is_optional: false,
789    }
790}
791
792impl<'a> LockbCursor<'a> {
793    fn new(bytes: &'a [u8]) -> Self {
794        Self { bytes, pos: 0 }
795    }
796
797    fn read_bytes(&mut self, len: usize) -> Result<&'a [u8], String> {
798        let end = self
799            .pos
800            .checked_add(len)
801            .ok_or_else(|| "bun.lockb offset overflow".to_string())?;
802        let slice = self
803            .bytes
804            .get(self.pos..end)
805            .ok_or_else(|| "bun.lockb is truncated".to_string())?;
806        self.pos = end;
807        Ok(slice)
808    }
809
810    fn expect_bytes(&mut self, expected: &[u8]) -> Result<(), String> {
811        let actual = self.read_bytes(expected.len())?;
812        if actual == expected {
813            Ok(())
814        } else {
815            Err("Invalid bun.lockb header".to_string())
816        }
817    }
818
819    fn read_u32(&mut self) -> Result<u32, String> {
820        let bytes: [u8; 4] = self
821            .read_bytes(4)?
822            .try_into()
823            .map_err(|_| "Invalid bun.lockb u32".to_string())?;
824        Ok(u32::from_le_bytes(bytes))
825    }
826
827    fn read_u64(&mut self) -> Result<u64, String> {
828        let bytes: [u8; 8] = self
829            .read_bytes(8)?
830            .try_into()
831            .map_err(|_| "Invalid bun.lockb u64".to_string())?;
832        Ok(u64::from_le_bytes(bytes))
833    }
834}
835
836crate::register_parser!(
837    "Legacy Bun binary lockfile",
838    &["**/bun.lockb"],
839    "npm",
840    "JavaScript",
841    Some("https://bun.sh/docs/pm/lockfile"),
842);