use std::fmt;
use std::path::Path;
const RAM_BUNDLE_MAGIC: u32 = 0xFB0B_D1E5;
const HEADER_SIZE: usize = 12;
const MODULE_ENTRY_SIZE: usize = 8;
#[derive(Debug)]
pub enum RamBundleError {
InvalidMagic,
TooShort,
InvalidEntry(String),
Io(std::io::Error),
SourceMap(srcmap_sourcemap::ParseError),
}
impl fmt::Display for RamBundleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidMagic => write!(f, "invalid RAM bundle magic number"),
Self::TooShort => write!(f, "data too short for RAM bundle header"),
Self::InvalidEntry(msg) => write!(f, "invalid module entry: {msg}"),
Self::Io(e) => write!(f, "I/O error: {e}"),
Self::SourceMap(e) => write!(f, "source map error: {e}"),
}
}
}
impl std::error::Error for RamBundleError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(e) => Some(e),
Self::SourceMap(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for RamBundleError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
impl From<srcmap_sourcemap::ParseError> for RamBundleError {
fn from(e: srcmap_sourcemap::ParseError) -> Self {
Self::SourceMap(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RamBundleType {
Indexed,
Unbundle,
}
#[derive(Debug, Clone)]
pub struct RamBundleModule {
pub id: u32,
pub source_code: String,
}
#[derive(Debug)]
pub struct IndexedRamBundle {
pub module_count: u32,
pub startup_code: String,
modules: Vec<Option<RamBundleModule>>,
}
impl IndexedRamBundle {
pub fn from_bytes(data: &[u8]) -> Result<Self, RamBundleError> {
if data.len() < HEADER_SIZE {
return Err(RamBundleError::TooShort);
}
let magic = read_u32_le(data, 0).unwrap();
if magic != RAM_BUNDLE_MAGIC {
return Err(RamBundleError::InvalidMagic);
}
let module_count = read_u32_le(data, 4).unwrap();
let startup_code_size = read_u32_le(data, 8).unwrap() as usize;
let table_size = (module_count as usize)
.checked_mul(MODULE_ENTRY_SIZE)
.ok_or(RamBundleError::TooShort)?;
let table_end = HEADER_SIZE.checked_add(table_size).ok_or(RamBundleError::TooShort)?;
if data.len() < table_end {
return Err(RamBundleError::TooShort);
}
let startup_start = table_end;
let startup_end =
startup_start.checked_add(startup_code_size).ok_or(RamBundleError::TooShort)?;
if data.len() < startup_end {
return Err(RamBundleError::TooShort);
}
let startup_code = std::str::from_utf8(&data[startup_start..startup_end])
.map_err(|e| {
RamBundleError::InvalidEntry(format!("startup code is not valid UTF-8: {e}"))
})?
.to_owned();
let modules_base = startup_end;
let mut modules = Vec::with_capacity(module_count as usize);
for i in 0..module_count as usize {
let entry_offset = HEADER_SIZE + i * MODULE_ENTRY_SIZE;
let offset = read_u32_le(data, entry_offset).unwrap() as usize;
let length = read_u32_le(data, entry_offset + 4).unwrap() as usize;
if offset == 0 && length == 0 {
modules.push(None);
continue;
}
let abs_start = modules_base.checked_add(offset).ok_or_else(|| {
RamBundleError::InvalidEntry(format!("module {i} offset overflows"))
})?;
let abs_end = abs_start.checked_add(length).ok_or_else(|| {
RamBundleError::InvalidEntry(format!("module {i} length overflows"))
})?;
if abs_end > data.len() {
return Err(RamBundleError::InvalidEntry(format!(
"module {i} extends beyond data (offset={offset}, length={length}, data_len={})",
data.len()
)));
}
let source_code = std::str::from_utf8(&data[abs_start..abs_end])
.map_err(|e| {
RamBundleError::InvalidEntry(format!(
"module {i} source is not valid UTF-8: {e}"
))
})?
.to_owned();
modules.push(Some(RamBundleModule { id: i as u32, source_code }));
}
Ok(Self { module_count, startup_code, modules })
}
pub fn module_count(&self) -> u32 {
self.module_count
}
pub fn get_module(&self, id: u32) -> Option<&RamBundleModule> {
self.modules.get(id as usize)?.as_ref()
}
pub fn modules(&self) -> impl Iterator<Item = &RamBundleModule> {
self.modules.iter().filter_map(|m| m.as_ref())
}
pub fn startup_code(&self) -> &str {
&self.startup_code
}
}
pub fn is_ram_bundle(data: &[u8]) -> bool {
read_u32_le(data, 0) == Some(RAM_BUNDLE_MAGIC)
}
pub fn is_unbundle_dir(path: &Path) -> bool {
path.join("js-modules").is_dir()
}
fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
if offset + 4 > data.len() {
return None;
}
Some(u32::from_le_bytes([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]))
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_bundle(modules: &[Option<&str>], startup: &str) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&RAM_BUNDLE_MAGIC.to_le_bytes());
data.extend_from_slice(&(modules.len() as u32).to_le_bytes());
data.extend_from_slice(&(startup.len() as u32).to_le_bytes());
let mut module_bodies: Vec<(u32, u32)> = Vec::new();
let mut current_offset: u32 = 0;
for module in modules {
match module {
Some(src) => {
let len = src.len() as u32;
module_bodies.push((current_offset, len));
current_offset += len;
}
None => {
module_bodies.push((0, 0));
}
}
}
for &(offset, length) in &module_bodies {
data.extend_from_slice(&offset.to_le_bytes());
data.extend_from_slice(&length.to_le_bytes());
}
data.extend_from_slice(startup.as_bytes());
for module in modules.iter().flatten() {
data.extend_from_slice(module.as_bytes());
}
data
}
#[test]
fn test_is_ram_bundle() {
let data = make_test_bundle(&[], "");
assert!(is_ram_bundle(&data));
}
#[test]
fn test_is_ram_bundle_wrong_magic() {
let data = [0x00, 0x00, 0x00, 0x00];
assert!(!is_ram_bundle(&data));
}
#[test]
fn test_is_ram_bundle_too_short() {
assert!(!is_ram_bundle(&[0xE5, 0xD1, 0x0B]));
assert!(!is_ram_bundle(&[]));
}
#[test]
fn test_parse_empty_bundle() {
let data = make_test_bundle(&[], "var x = 1;");
let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
assert_eq!(bundle.module_count(), 0);
assert_eq!(bundle.startup_code(), "var x = 1;");
assert_eq!(bundle.modules().count(), 0);
}
#[test]
fn test_parse_single_module() {
let data = make_test_bundle(&[Some("__d(function(){},0);")], "startup();");
let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
assert_eq!(bundle.module_count(), 1);
assert_eq!(bundle.startup_code(), "startup();");
let module = bundle.get_module(0).unwrap();
assert_eq!(module.id, 0);
assert_eq!(module.source_code, "__d(function(){},0);");
}
#[test]
fn test_parse_multiple_modules() {
let modules = vec![
Some("__d(function(){console.log('a')},0);"),
Some("__d(function(){console.log('b')},1);"),
Some("__d(function(){console.log('c')},2);"),
];
let data = make_test_bundle(&modules, "require(0);");
let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
assert_eq!(bundle.module_count(), 3);
assert_eq!(bundle.startup_code(), "require(0);");
for (i, module) in bundle.modules().enumerate() {
assert_eq!(module.id, i as u32);
assert!(module.source_code.contains(&format!("'{}'", (b'a' + i as u8) as char)));
}
}
#[test]
fn test_empty_module_slots() {
let modules = vec![Some("__d(function(){},0);"), None, Some("__d(function(){},2);")];
let data = make_test_bundle(&modules, "");
let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
assert_eq!(bundle.module_count(), 3);
assert!(bundle.get_module(0).is_some());
assert!(bundle.get_module(1).is_none());
assert!(bundle.get_module(2).is_some());
assert_eq!(bundle.modules().count(), 2);
}
#[test]
fn test_get_module_out_of_range() {
let data = make_test_bundle(&[Some("__d(function(){},0);")], "");
let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
assert!(bundle.get_module(0).is_some());
assert!(bundle.get_module(1).is_none());
assert!(bundle.get_module(999).is_none());
}
#[test]
fn test_invalid_magic() {
let mut data = make_test_bundle(&[], "");
data[0] = 0x00;
let err = IndexedRamBundle::from_bytes(&data).unwrap_err();
assert!(matches!(err, RamBundleError::InvalidMagic));
}
#[test]
fn test_too_short_header() {
let err = IndexedRamBundle::from_bytes(&[0xE5, 0xD1, 0x0B, 0xFB]).unwrap_err();
assert!(matches!(err, RamBundleError::TooShort));
}
#[test]
fn test_too_short_for_table() {
let mut data = Vec::new();
data.extend_from_slice(&RAM_BUNDLE_MAGIC.to_le_bytes());
data.extend_from_slice(&1000_u32.to_le_bytes());
data.extend_from_slice(&0_u32.to_le_bytes());
let err = IndexedRamBundle::from_bytes(&data).unwrap_err();
assert!(matches!(err, RamBundleError::TooShort));
}
#[test]
fn test_module_extends_beyond_data() {
let data = make_test_bundle(&[Some("hello world")], "");
let truncated = &data[..data.len() - 5];
let err = IndexedRamBundle::from_bytes(truncated).unwrap_err();
assert!(matches!(err, RamBundleError::InvalidEntry(_)));
}
#[test]
fn test_module_iteration_order() {
let modules = vec![Some("mod0"), None, Some("mod2"), None, Some("mod4")];
let data = make_test_bundle(&modules, "");
let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
let ids: Vec<u32> = bundle.modules().map(|m| m.id).collect();
assert_eq!(ids, vec![0, 2, 4]);
}
#[test]
fn test_is_unbundle_dir_nonexistent() {
assert!(!is_unbundle_dir(Path::new("/nonexistent/path")));
}
#[test]
fn test_display_errors() {
assert_eq!(RamBundleError::InvalidMagic.to_string(), "invalid RAM bundle magic number");
assert_eq!(RamBundleError::TooShort.to_string(), "data too short for RAM bundle header");
assert_eq!(
RamBundleError::InvalidEntry("bad".to_string()).to_string(),
"invalid module entry: bad"
);
}
#[test]
fn test_ram_bundle_type_equality() {
assert_eq!(RamBundleType::Indexed, RamBundleType::Indexed);
assert_eq!(RamBundleType::Unbundle, RamBundleType::Unbundle);
assert_ne!(RamBundleType::Indexed, RamBundleType::Unbundle);
}
}