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