sourcemap/
ram_bundle.rs

1//! RAM bundle operations
2use 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
16/// Magic number for RAM bundles
17pub const RAM_BUNDLE_MAGIC: u32 = 0xFB0B_D1E5;
18
19const JS_MODULES_DIR_NAME: &str = "js-modules";
20
21/// Represents a RAM bundle header
22#[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    /// Checks if the magic matches.
32    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/// Represents an indexed RAM bundle module
51///
52/// This type is used on iOS by default.
53#[derive(Debug)]
54pub struct RamBundleModule<'a> {
55    id: usize,
56    data: &'a [u8],
57}
58
59impl<'a> RamBundleModule<'a> {
60    /// Returns the integer ID of the module.
61    pub fn id(&self) -> usize {
62        self.id
63    }
64
65    /// Returns a slice to the data in the module.
66    pub fn data(&self) -> &'a [u8] {
67        self.data
68    }
69
70    /// Returns a source view of the data.
71    ///
72    /// This operation fails if the source code is not valid UTF-8.
73    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
81/// An iterator over modules in a RAM bundle
82pub 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/// The type of ram bundle.
103#[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 RAM bundle
112    Indexed(IndexedRamBundle<'a>),
113    /// File (unbundle) RAM bundle
114    Unbundle(UnbundleRamBundle),
115}
116
117/// The main RAM bundle interface
118#[derive(Debug, Clone)]
119pub struct RamBundle<'a> {
120    repr: RamBundleImpl<'a>,
121}
122
123impl<'a> RamBundle<'a> {
124    /// Parses an indexed RAM bundle from the given slice
125    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    /// Parses an indexed RAM bundle from the given vector
132    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    /// Creates a new indexed RAM bundle from the file path
139    pub fn parse_indexed_from_path(path: &Path) -> Result<Self> {
140        RamBundle::parse_indexed_from_vec(fs::read(path)?)
141    }
142
143    /// Creates a file (unbundle) RAM bundle from the path
144    ///
145    /// The provided path should point to a javascript file, that serves
146    /// as an entry point (startup code) for the app. The modules are stored in js-modules/
147    /// directory, next to the entry point. The js-modules/ directory must ONLY contain
148    /// files with integer names and the ".js" file suffix, along with the UNBUNDLE magic file.
149    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    /// Returns the type of the RAM bundle.
156    pub fn bundle_type(&self) -> RamBundleType {
157        match self.repr {
158            RamBundleImpl::Indexed(..) => RamBundleType::Indexed,
159            RamBundleImpl::Unbundle(..) => RamBundleType::Unbundle,
160        }
161    }
162
163    /// Looks up a module by ID in the bundle
164    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    /// Returns the number of modules in the bundle
172    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    /// Returns the startup code
180    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    /// Returns an iterator over all modules in the bundle
187    pub fn iter_modules(&self) -> RamBundleModuleIter {
188        RamBundleModuleIter {
189            range: 0..self.module_count(),
190            ram_bundle: self,
191        }
192    }
193}
194
195/// Filename must be made of ascii-only digits and the .js extension
196/// Anything else errors with `Error::InvalidRamBundleIndex`
197fn 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/// Represents a file RAM bundle
206///
207/// This RAM bundle type is mostly used on Android.
208#[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    /// Returns the number of modules in the bundle
260    pub fn module_count(&self) -> usize {
261        self.module_count
262    }
263
264    /// Returns the startup code
265    pub fn startup_code(&self) -> Result<&[u8]> {
266        Ok(&self.startup_code)
267    }
268
269    /// Looks up a module by ID in the bundle
270    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/// Represents an indexed RAM bundle
279///
280/// Provides access to a react-native metro
281/// [RAM bundle](https://facebook.github.io/metro/docs/en/bundling).
282#[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    /// Parses a RAM bundle from a given slice of bytes.
292    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    /// Returns the number of modules in the bundle
311    pub fn module_count(&self) -> usize {
312        self.module_count
313    }
314
315    /// Returns the startup code
316    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    /// Looks up a module by ID in the bundle
323    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        // Strip the trailing NULL byte
346        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
353/// An iterator over deconstructed RAM bundle sources and sourcemaps
354pub 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
437/// Deconstructs a RAM bundle into a sequence of sources and their sourcemaps
438///
439/// With the help of the RAM bundle's indexed sourcemap, the bundle is split into modules,
440/// where each module is represented by its minified source and the corresponding sourcemap that
441/// we recover from the initial indexed sourcemap.
442pub 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
456/// Checks if the given byte slice contains an indexed RAM bundle
457pub 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
464/// Returns "true" if the given path points to the startup file of a file RAM bundle
465///
466/// The method checks the directory structure and the magic number in UNBUNDLE file.
467pub 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        // Header checks
517        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        // Check first modules
523        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        // Modules 0, 3, 4
568        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}