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