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);