1use scroll::Pread;
3use std::borrow::Cow;
4use std::collections::BTreeMap;
5use std::fs;
6use std::fs::File;
7use std::io::Read;
8use std::ops::Range;
9use std::path::Path;
10
11use crate::builder::SourceMapBuilder;
12use crate::errors::{Error, Result};
13use crate::sourceview::SourceView;
14use crate::types::{SourceMap, SourceMapIndex};
15
16pub const RAM_BUNDLE_MAGIC: u32 = 0xFB0B_D1E5;
18
19const JS_MODULES_DIR_NAME: &str = "js-modules";
20
21#[derive(Debug, Pread, Clone, Copy)]
23#[repr(C, packed)]
24pub struct RamBundleHeader {
25 magic: u32,
26 module_count: u32,
27 startup_code_size: u32,
28}
29
30impl RamBundleHeader {
31 pub fn is_valid_magic(&self) -> bool {
33 self.magic == RAM_BUNDLE_MAGIC
34 }
35}
36
37#[derive(Debug, Pread, Clone, Copy)]
38#[repr(C, packed)]
39struct ModuleEntry {
40 offset: u32,
41 length: u32,
42}
43
44impl ModuleEntry {
45 pub fn is_empty(self) -> bool {
46 self.offset == 0 && self.length == 0
47 }
48}
49
50#[derive(Debug)]
54pub struct RamBundleModule<'a> {
55 id: usize,
56 data: &'a [u8],
57}
58
59impl<'a> RamBundleModule<'a> {
60 pub fn id(&self) -> usize {
62 self.id
63 }
64
65 pub fn data(&self) -> &'a [u8] {
67 self.data
68 }
69
70 pub fn source_view(&self) -> Result<SourceView> {
74 match std::str::from_utf8(self.data) {
75 Ok(s) => Ok(SourceView::new(s.into())),
76 Err(e) => Err(Error::Utf8(e)),
77 }
78 }
79}
80
81pub struct RamBundleModuleIter<'a> {
83 range: Range<usize>,
84 ram_bundle: &'a RamBundle<'a>,
85}
86
87impl<'a> Iterator for RamBundleModuleIter<'a> {
88 type Item = Result<RamBundleModule<'a>>;
89
90 fn next(&mut self) -> Option<Self::Item> {
91 for next_id in self.range.by_ref() {
92 match self.ram_bundle.get_module(next_id) {
93 Ok(None) => continue,
94 Ok(Some(module)) => return Some(Ok(module)),
95 Err(e) => return Some(Err(e)),
96 }
97 }
98 None
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
104pub enum RamBundleType {
105 Indexed,
106 Unbundle,
107}
108
109#[derive(Debug, Clone)]
110enum RamBundleImpl<'a> {
111 Indexed(IndexedRamBundle<'a>),
113 Unbundle(UnbundleRamBundle),
115}
116
117#[derive(Debug, Clone)]
119pub struct RamBundle<'a> {
120 repr: RamBundleImpl<'a>,
121}
122
123impl<'a> RamBundle<'a> {
124 pub fn parse_indexed_from_slice(bytes: &'a [u8]) -> Result<Self> {
126 Ok(RamBundle {
127 repr: RamBundleImpl::Indexed(IndexedRamBundle::parse(Cow::Borrowed(bytes))?),
128 })
129 }
130
131 pub fn parse_indexed_from_vec(bytes: Vec<u8>) -> Result<Self> {
133 Ok(RamBundle {
134 repr: RamBundleImpl::Indexed(IndexedRamBundle::parse(Cow::Owned(bytes))?),
135 })
136 }
137
138 pub fn parse_indexed_from_path(path: &Path) -> Result<Self> {
140 RamBundle::parse_indexed_from_vec(fs::read(path)?)
141 }
142
143 pub fn parse_unbundle_from_path(bundle_path: &Path) -> Result<Self> {
150 Ok(RamBundle {
151 repr: RamBundleImpl::Unbundle(UnbundleRamBundle::parse(bundle_path)?),
152 })
153 }
154
155 pub fn bundle_type(&self) -> RamBundleType {
157 match self.repr {
158 RamBundleImpl::Indexed(..) => RamBundleType::Indexed,
159 RamBundleImpl::Unbundle(..) => RamBundleType::Unbundle,
160 }
161 }
162
163 pub fn get_module(&self, id: usize) -> Result<Option<RamBundleModule>> {
165 match self.repr {
166 RamBundleImpl::Indexed(ref indexed) => indexed.get_module(id),
167 RamBundleImpl::Unbundle(ref file) => file.get_module(id),
168 }
169 }
170
171 pub fn module_count(&self) -> usize {
173 match self.repr {
174 RamBundleImpl::Indexed(ref indexed) => indexed.module_count(),
175 RamBundleImpl::Unbundle(ref file) => file.module_count(),
176 }
177 }
178
179 pub fn startup_code(&self) -> Result<&[u8]> {
181 match self.repr {
182 RamBundleImpl::Indexed(ref indexed) => indexed.startup_code(),
183 RamBundleImpl::Unbundle(ref file) => file.startup_code(),
184 }
185 }
186 pub fn iter_modules(&self) -> RamBundleModuleIter {
188 RamBundleModuleIter {
189 range: 0..self.module_count(),
190 ram_bundle: self,
191 }
192 }
193}
194
195fn js_filename_to_index_strict(filename: &str) -> Result<usize> {
198 match filename.strip_suffix(".js") {
199 Some(basename) => basename
200 .parse::<usize>()
201 .or(Err(Error::InvalidRamBundleIndex)),
202 None => Err(Error::InvalidRamBundleIndex),
203 }
204}
205#[derive(Debug, Clone)]
209struct UnbundleRamBundle {
210 startup_code: Vec<u8>,
211 module_count: usize,
212 modules: BTreeMap<usize, Vec<u8>>,
213}
214
215impl UnbundleRamBundle {
216 pub fn parse(bundle_path: &Path) -> Result<Self> {
217 if !is_unbundle_path(bundle_path) {
218 return Err(Error::NotARamBundle);
219 }
220
221 let bundle_dir = match bundle_path.parent() {
222 Some(dir) => dir,
223 None => return Err(Error::NotARamBundle),
224 };
225
226 let startup_code = fs::read(bundle_path)?;
227 let mut max_module_id = 0;
228 let mut modules: BTreeMap<usize, Vec<u8>> = Default::default();
229
230 let js_modules_dir = bundle_dir.join(JS_MODULES_DIR_NAME);
231
232 for entry in js_modules_dir.read_dir()? {
233 let entry = entry?;
234 if !entry.file_type()?.is_file() {
235 continue;
236 }
237
238 let path = entry.path();
239 let filename_os = path.file_name().unwrap();
240 let filename: &str = &filename_os.to_string_lossy();
241 if filename == "UNBUNDLE" {
242 continue;
243 }
244 let module_id = js_filename_to_index_strict(filename)?;
245 if module_id > max_module_id {
246 max_module_id = module_id;
247 }
248
249 modules.insert(module_id, fs::read(path)?);
250 }
251
252 Ok(UnbundleRamBundle {
253 startup_code,
254 modules,
255 module_count: max_module_id + 1,
256 })
257 }
258
259 pub fn module_count(&self) -> usize {
261 self.module_count
262 }
263
264 pub fn startup_code(&self) -> Result<&[u8]> {
266 Ok(&self.startup_code)
267 }
268
269 pub fn get_module(&self, id: usize) -> Result<Option<RamBundleModule>> {
271 match self.modules.get(&id) {
272 Some(data) => Ok(Some(RamBundleModule { id, data })),
273 None => Ok(None),
274 }
275 }
276}
277
278#[derive(Debug, Clone)]
283struct IndexedRamBundle<'a> {
284 bytes: Cow<'a, [u8]>,
285 module_count: usize,
286 startup_code_size: usize,
287 startup_code_offset: usize,
288}
289
290impl<'a> IndexedRamBundle<'a> {
291 pub fn parse(bytes: Cow<'a, [u8]>) -> Result<Self> {
293 let header = bytes.pread_with::<RamBundleHeader>(0, scroll::LE)?;
294
295 if !header.is_valid_magic() {
296 return Err(Error::InvalidRamBundleMagic);
297 }
298
299 let module_count = header.module_count as usize;
300 let startup_code_offset = std::mem::size_of::<RamBundleHeader>()
301 + module_count * std::mem::size_of::<ModuleEntry>();
302 Ok(IndexedRamBundle {
303 bytes,
304 module_count,
305 startup_code_size: header.startup_code_size as usize,
306 startup_code_offset,
307 })
308 }
309
310 pub fn module_count(&self) -> usize {
312 self.module_count
313 }
314
315 pub fn startup_code(&self) -> Result<&[u8]> {
317 self.bytes
318 .pread_with(self.startup_code_offset, self.startup_code_size)
319 .map_err(Error::Scroll)
320 }
321
322 pub fn get_module(&self, id: usize) -> Result<Option<RamBundleModule>> {
324 if id >= self.module_count {
325 return Err(Error::InvalidRamBundleIndex);
326 }
327
328 let entry_offset =
329 std::mem::size_of::<RamBundleHeader>() + id * std::mem::size_of::<ModuleEntry>();
330
331 let module_entry = self
332 .bytes
333 .pread_with::<ModuleEntry>(entry_offset, scroll::LE)?;
334
335 if module_entry.is_empty() {
336 return Ok(None);
337 }
338
339 let module_global_offset = self.startup_code_offset + module_entry.offset as usize;
340
341 if module_entry.length == 0 {
342 return Err(Error::InvalidRamBundleEntry);
343 }
344
345 let module_length = (module_entry.length - 1) as usize;
347 let data = self.bytes.pread_with(module_global_offset, module_length)?;
348
349 Ok(Some(RamBundleModule { id, data }))
350 }
351}
352
353pub struct SplitRamBundleModuleIter<'a> {
355 ram_bundle_iter: RamBundleModuleIter<'a>,
356 sm: SourceMap,
357 offsets: Vec<Option<u32>>,
358}
359
360impl<'a> SplitRamBundleModuleIter<'a> {
361 fn split_module(
362 &self,
363 module: RamBundleModule<'a>,
364 ) -> Result<Option<(String, SourceView, SourceMap)>> {
365 let module_offset = self
366 .offsets
367 .get(module.id())
368 .ok_or(Error::InvalidRamBundleIndex)?;
369 let starting_line = match *module_offset {
370 Some(offset) => offset,
371 None => return Ok(None),
372 };
373
374 let mut token_iter = self.sm.tokens();
375
376 if !token_iter.seek(starting_line, 0) {
377 return Err(Error::InvalidRamBundleEntry);
378 }
379
380 let source: SourceView = module.source_view()?;
381 let line_count = source.line_count() as u32;
382 let ending_line = starting_line + line_count;
383 let last_line_len = source
384 .get_line(line_count - 1)
385 .map_or(0, |line| line.chars().map(char::len_utf16).sum())
386 as u32;
387
388 let filename = format!("{}.js", module.id);
389 let mut builder = SourceMapBuilder::new(Some(&filename));
390 for token in token_iter {
391 let dst_line = token.get_dst_line();
392 let dst_col = token.get_dst_col();
393
394 if dst_line >= ending_line || dst_col >= last_line_len {
395 break;
396 }
397
398 let raw = builder.add(
399 dst_line - starting_line,
400 dst_col,
401 token.get_src_line(),
402 token.get_src_col(),
403 token.get_source(),
404 token.get_name(),
405 false,
406 );
407 if token.get_source().is_some() && !builder.has_source_contents(raw.src_id) {
408 builder.set_source_contents(
409 raw.src_id,
410 self.sm.get_source_contents(token.get_src_id()),
411 );
412 }
413 }
414 let sourcemap = builder.into_sourcemap();
415 Ok(Some((filename, source, sourcemap)))
416 }
417}
418
419impl Iterator for SplitRamBundleModuleIter<'_> {
420 type Item = Result<(String, SourceView, SourceMap)>;
421
422 fn next(&mut self) -> Option<Self::Item> {
423 while let Some(module_result) = self.ram_bundle_iter.next() {
424 match module_result {
425 Ok(module) => match self.split_module(module) {
426 Ok(None) => continue,
427 Ok(Some(result_tuple)) => return Some(Ok(result_tuple)),
428 Err(_) => return Some(Err(Error::InvalidRamBundleEntry)),
429 },
430 Err(_) => return Some(Err(Error::InvalidRamBundleEntry)),
431 }
432 }
433 None
434 }
435}
436
437pub fn split_ram_bundle<'a>(
443 ram_bundle: &'a RamBundle,
444 smi: &SourceMapIndex,
445) -> Result<SplitRamBundleModuleIter<'a>> {
446 Ok(SplitRamBundleModuleIter {
447 ram_bundle_iter: ram_bundle.iter_modules(),
448 sm: smi.flatten()?,
449 offsets: smi
450 .x_facebook_offsets()
451 .map(|v| v.to_vec())
452 .ok_or(Error::NotARamBundle)?,
453 })
454}
455
456pub fn is_ram_bundle_slice(slice: &[u8]) -> bool {
458 slice
459 .pread_with::<RamBundleHeader>(0, scroll::LE)
460 .ok()
461 .is_some_and(|x| x.is_valid_magic())
462}
463
464pub fn is_unbundle_path(bundle_path: &Path) -> bool {
468 if !bundle_path.is_file() {
469 return false;
470 }
471
472 let bundle_dir = match bundle_path.parent() {
473 Some(dir) => dir,
474 None => return false,
475 };
476
477 let unbundle_file_path = bundle_dir.join(JS_MODULES_DIR_NAME).join("UNBUNDLE");
478 if !unbundle_file_path.is_file() {
479 return false;
480 }
481 let mut unbundle_file = match File::open(unbundle_file_path) {
482 Ok(file) => file,
483 Err(_) => return false,
484 };
485
486 let mut bundle_magic = [0; 4];
487 if unbundle_file.read_exact(&mut bundle_magic).is_err() {
488 return false;
489 }
490
491 bundle_magic == RAM_BUNDLE_MAGIC.to_le_bytes()
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use std::fs::File;
498 use std::io::Read;
499
500 #[test]
501 fn test_indexed_ram_bundle_parse() -> std::result::Result<(), Box<dyn std::error::Error>> {
502 let mut bundle_file =
503 File::open("./tests/fixtures/ram_bundle/indexed_bundle_1/basic.jsbundle")?;
504 let mut bundle_data = Vec::new();
505 bundle_file.read_to_end(&mut bundle_data)?;
506 assert!(is_ram_bundle_slice(&bundle_data));
507 let ram_bundle = RamBundle::parse_indexed_from_slice(&bundle_data)?;
508
509 let indexed_ram_bundle = match ram_bundle.repr.clone() {
510 RamBundleImpl::Indexed(bundle) => bundle,
511 _ => {
512 panic!("Invalid RamBundleImpl type");
513 }
514 };
515
516 assert_eq!(indexed_ram_bundle.startup_code_size, 0x7192);
518 assert_eq!(indexed_ram_bundle.startup_code_offset, 0x34);
519
520 assert_eq!(ram_bundle.module_count(), 5);
521
522 let mut module_iter = ram_bundle.iter_modules();
524
525 let module_0 = module_iter.next().unwrap()?;
526 let module_0_data = module_0.data();
527 assert_eq!(module_0.id(), 0);
528 assert_eq!(module_0_data.len(), 0xa8 - 1);
529 assert_eq!(
530 &module_0_data[0..60],
531 "__d(function(g,r,i,a,m,e,d){\"use strict\";const o=r(d[0]),s=r".as_bytes()
532 );
533
534 let module_3 = module_iter.next().unwrap()?;
535 let module_3_data = module_3.data();
536 assert_eq!(module_3.id(), 3);
537 assert_eq!(module_3_data.len(), 0x6b - 1);
538 assert_eq!(
539 &module_3_data[0..60],
540 "__d(function(g,r,i,a,m,e,d){\"use strict\";console.log('inside".as_bytes()
541 );
542
543 let module_1 = ram_bundle.get_module(1)?;
544 assert!(module_1.is_none());
545
546 Ok(())
547 }
548
549 #[test]
550 fn test_indexed_ram_bundle_split() -> std::result::Result<(), Box<dyn std::error::Error>> {
551 let ram_bundle = RamBundle::parse_indexed_from_path(Path::new(
552 "./tests/fixtures/ram_bundle/indexed_bundle_1/basic.jsbundle",
553 ))?;
554
555 let sourcemap_file =
556 File::open("./tests/fixtures/ram_bundle/indexed_bundle_1/basic.jsbundle.map")?;
557 let ism = SourceMapIndex::from_reader(sourcemap_file)?;
558
559 assert!(ism.is_for_ram_bundle());
560
561 let x_facebook_offsets = ism.x_facebook_offsets().unwrap();
562 assert_eq!(x_facebook_offsets.len(), 5);
563
564 let x_metro_module_paths = ism.x_metro_module_paths().unwrap();
565 assert_eq!(x_metro_module_paths.len(), 7);
566
567 assert_eq!(split_ram_bundle(&ram_bundle, &ism)?.count(), 3);
569
570 let mut ram_bundle_iter = split_ram_bundle(&ram_bundle, &ism)?;
571
572 let (name, sourceview, sourcemap) = ram_bundle_iter.next().unwrap()?;
573 assert_eq!(name, "0.js");
574 assert_eq!(
575 &sourceview.source()[0..60],
576 "__d(function(g,r,i,a,m,e,d){\"use strict\";const o=r(d[0]),s=r"
577 );
578 assert_eq!(
579 &sourcemap.get_source_contents(0).unwrap()[0..60],
580 "const f = require(\"./other\");\nconst isWindows = require(\"is-"
581 );
582
583 Ok(())
584 }
585
586 #[test]
587 fn test_file_ram_bundle_parse() -> std::result::Result<(), Box<dyn std::error::Error>> {
588 let valid_bundle_path = Path::new("./tests/fixtures/ram_bundle/file_bundle_1/basic.bundle");
589 assert!(is_unbundle_path(valid_bundle_path));
590
591 assert!(!is_unbundle_path(Path::new("./tmp/invalid/bundle/path")));
592
593 let ram_bundle = RamBundle::parse_unbundle_from_path(valid_bundle_path)?;
594
595 match ram_bundle.repr {
596 RamBundleImpl::Unbundle(_) => (),
597 _ => {
598 panic!("Invalid RamBundleImpl type");
599 }
600 };
601
602 assert_eq!(ram_bundle.module_count(), 4);
603
604 let startup_code = ram_bundle.startup_code()?;
605 assert_eq!(
606 startup_code[0..60].to_vec(),
607 b"var __DEV__=false,__BUNDLE_START_TIME__=this.nativePerforman".to_vec()
608 );
609
610 let module_0 = ram_bundle.get_module(0)?.unwrap();
611 let module_0_data = module_0.data();
612 assert_eq!(
613 module_0_data[0..60].to_vec(),
614 b"__d(function(g,r,i,a,m,e,d){'use strict';var t=Date.now();r(".to_vec()
615 );
616
617 let module_1 = ram_bundle.get_module(1)?;
618 assert!(module_1.is_none());
619
620 Ok(())
621 }
622}