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