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