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