Skip to main content

provenant/parsers/
bun_lockb.rs

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