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