1use std::fmt;
41use std::path::Path;
42
43const RAM_BUNDLE_MAGIC: u32 = 0xFB0BD1E5;
45
46const HEADER_SIZE: usize = 12;
48
49const MODULE_ENTRY_SIZE: usize = 8;
51
52#[derive(Debug)]
54pub enum RamBundleError {
55 InvalidMagic,
57 TooShort,
59 InvalidEntry(String),
61 Io(std::io::Error),
63 SourceMap(srcmap_sourcemap::ParseError),
65}
66
67impl fmt::Display for RamBundleError {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 Self::InvalidMagic => write!(f, "invalid RAM bundle magic number"),
71 Self::TooShort => write!(f, "data too short for RAM bundle header"),
72 Self::InvalidEntry(msg) => write!(f, "invalid module entry: {msg}"),
73 Self::Io(e) => write!(f, "I/O error: {e}"),
74 Self::SourceMap(e) => write!(f, "source map error: {e}"),
75 }
76 }
77}
78
79impl std::error::Error for RamBundleError {
80 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
81 match self {
82 Self::Io(e) => Some(e),
83 Self::SourceMap(e) => Some(e),
84 _ => None,
85 }
86 }
87}
88
89impl From<std::io::Error> for RamBundleError {
90 fn from(e: std::io::Error) -> Self {
91 Self::Io(e)
92 }
93}
94
95impl From<srcmap_sourcemap::ParseError> for RamBundleError {
96 fn from(e: srcmap_sourcemap::ParseError) -> Self {
97 Self::SourceMap(e)
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum RamBundleType {
104 Indexed,
106 Unbundle,
108}
109
110#[derive(Debug, Clone)]
112pub struct RamBundleModule {
113 pub id: u32,
115 pub source_code: String,
117}
118
119#[derive(Debug)]
124pub struct IndexedRamBundle {
125 pub module_count: u32,
127 pub startup_code: String,
129 modules: Vec<Option<RamBundleModule>>,
131}
132
133impl IndexedRamBundle {
134 pub fn from_bytes(data: &[u8]) -> Result<Self, RamBundleError> {
144 if data.len() < HEADER_SIZE {
145 return Err(RamBundleError::TooShort);
146 }
147
148 let magic = read_u32_le(data, 0).unwrap();
149 if magic != RAM_BUNDLE_MAGIC {
150 return Err(RamBundleError::InvalidMagic);
151 }
152
153 let module_count = read_u32_le(data, 4).unwrap();
154 let startup_code_size = read_u32_le(data, 8).unwrap() as usize;
155
156 let table_size = (module_count as usize)
157 .checked_mul(MODULE_ENTRY_SIZE)
158 .ok_or(RamBundleError::TooShort)?;
159 let table_end = HEADER_SIZE
160 .checked_add(table_size)
161 .ok_or(RamBundleError::TooShort)?;
162
163 if data.len() < table_end {
164 return Err(RamBundleError::TooShort);
165 }
166
167 let startup_start = table_end;
169 let startup_end = startup_start
170 .checked_add(startup_code_size)
171 .ok_or(RamBundleError::TooShort)?;
172
173 if data.len() < startup_end {
174 return Err(RamBundleError::TooShort);
175 }
176
177 let startup_code = std::str::from_utf8(&data[startup_start..startup_end])
178 .map_err(|e| {
179 RamBundleError::InvalidEntry(format!("startup code is not valid UTF-8: {e}"))
180 })?
181 .to_owned();
182
183 let modules_base = startup_end;
185
186 let mut modules = Vec::with_capacity(module_count as usize);
187
188 for i in 0..module_count as usize {
189 let entry_offset = HEADER_SIZE + i * MODULE_ENTRY_SIZE;
190 let offset = read_u32_le(data, entry_offset).unwrap() as usize;
191 let length = read_u32_le(data, entry_offset + 4).unwrap() as usize;
192
193 if offset == 0 && length == 0 {
194 modules.push(None);
195 continue;
196 }
197
198 let abs_start = modules_base.checked_add(offset).ok_or_else(|| {
199 RamBundleError::InvalidEntry(format!("module {i} offset overflows"))
200 })?;
201 let abs_end = abs_start.checked_add(length).ok_or_else(|| {
202 RamBundleError::InvalidEntry(format!("module {i} length overflows"))
203 })?;
204
205 if abs_end > data.len() {
206 return Err(RamBundleError::InvalidEntry(format!(
207 "module {i} extends beyond data (offset={offset}, length={length}, data_len={})",
208 data.len()
209 )));
210 }
211
212 let source_code = std::str::from_utf8(&data[abs_start..abs_end])
213 .map_err(|e| {
214 RamBundleError::InvalidEntry(format!(
215 "module {i} source is not valid UTF-8: {e}"
216 ))
217 })?
218 .to_owned();
219
220 modules.push(Some(RamBundleModule {
221 id: i as u32,
222 source_code,
223 }));
224 }
225
226 Ok(Self {
227 module_count,
228 startup_code,
229 modules,
230 })
231 }
232
233 pub fn module_count(&self) -> u32 {
235 self.module_count
236 }
237
238 pub fn get_module(&self, id: u32) -> Option<&RamBundleModule> {
240 self.modules.get(id as usize)?.as_ref()
241 }
242
243 pub fn modules(&self) -> impl Iterator<Item = &RamBundleModule> {
245 self.modules.iter().filter_map(|m| m.as_ref())
246 }
247
248 pub fn startup_code(&self) -> &str {
250 &self.startup_code
251 }
252}
253
254pub fn is_ram_bundle(data: &[u8]) -> bool {
258 read_u32_le(data, 0) == Some(RAM_BUNDLE_MAGIC)
259}
260
261pub fn is_unbundle_dir(path: &Path) -> bool {
265 path.join("js-modules").is_dir()
266}
267
268fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
270 if offset + 4 > data.len() {
271 return None;
272 }
273 Some(u32::from_le_bytes([
274 data[offset],
275 data[offset + 1],
276 data[offset + 2],
277 data[offset + 3],
278 ]))
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 fn make_test_bundle(modules: &[Option<&str>], startup: &str) -> Vec<u8> {
290 let mut data = Vec::new();
291
292 data.extend_from_slice(&RAM_BUNDLE_MAGIC.to_le_bytes());
294 data.extend_from_slice(&(modules.len() as u32).to_le_bytes());
295 data.extend_from_slice(&(startup.len() as u32).to_le_bytes());
296
297 let mut module_bodies: Vec<(u32, u32)> = Vec::new();
301 let mut current_offset: u32 = 0;
302
303 for module in modules {
304 match module {
305 Some(src) => {
306 let len = src.len() as u32;
307 module_bodies.push((current_offset, len));
308 current_offset += len;
309 }
310 None => {
311 module_bodies.push((0, 0));
312 }
313 }
314 }
315
316 for &(offset, length) in &module_bodies {
318 data.extend_from_slice(&offset.to_le_bytes());
319 data.extend_from_slice(&length.to_le_bytes());
320 }
321
322 data.extend_from_slice(startup.as_bytes());
324
325 for module in modules.iter().flatten() {
327 data.extend_from_slice(module.as_bytes());
328 }
329
330 data
331 }
332
333 #[test]
334 fn test_is_ram_bundle() {
335 let data = make_test_bundle(&[], "");
336 assert!(is_ram_bundle(&data));
337 }
338
339 #[test]
340 fn test_is_ram_bundle_wrong_magic() {
341 let data = [0x00, 0x00, 0x00, 0x00];
342 assert!(!is_ram_bundle(&data));
343 }
344
345 #[test]
346 fn test_is_ram_bundle_too_short() {
347 assert!(!is_ram_bundle(&[0xE5, 0xD1, 0x0B]));
348 assert!(!is_ram_bundle(&[]));
349 }
350
351 #[test]
352 fn test_parse_empty_bundle() {
353 let data = make_test_bundle(&[], "var x = 1;");
354 let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
355 assert_eq!(bundle.module_count(), 0);
356 assert_eq!(bundle.startup_code(), "var x = 1;");
357 assert_eq!(bundle.modules().count(), 0);
358 }
359
360 #[test]
361 fn test_parse_single_module() {
362 let data = make_test_bundle(&[Some("__d(function(){},0);")], "startup();");
363 let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
364
365 assert_eq!(bundle.module_count(), 1);
366 assert_eq!(bundle.startup_code(), "startup();");
367
368 let module = bundle.get_module(0).unwrap();
369 assert_eq!(module.id, 0);
370 assert_eq!(module.source_code, "__d(function(){},0);");
371 }
372
373 #[test]
374 fn test_parse_multiple_modules() {
375 let modules = vec![
376 Some("__d(function(){console.log('a')},0);"),
377 Some("__d(function(){console.log('b')},1);"),
378 Some("__d(function(){console.log('c')},2);"),
379 ];
380 let data = make_test_bundle(&modules, "require(0);");
381 let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
382
383 assert_eq!(bundle.module_count(), 3);
384 assert_eq!(bundle.startup_code(), "require(0);");
385
386 for (i, module) in bundle.modules().enumerate() {
387 assert_eq!(module.id, i as u32);
388 assert!(
389 module
390 .source_code
391 .contains(&format!("'{}'", (b'a' + i as u8) as char))
392 );
393 }
394 }
395
396 #[test]
397 fn test_empty_module_slots() {
398 let modules = vec![
399 Some("__d(function(){},0);"),
400 None,
401 Some("__d(function(){},2);"),
402 ];
403 let data = make_test_bundle(&modules, "");
404 let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
405
406 assert_eq!(bundle.module_count(), 3);
407 assert!(bundle.get_module(0).is_some());
408 assert!(bundle.get_module(1).is_none());
409 assert!(bundle.get_module(2).is_some());
410
411 assert_eq!(bundle.modules().count(), 2);
413 }
414
415 #[test]
416 fn test_get_module_out_of_range() {
417 let data = make_test_bundle(&[Some("__d(function(){},0);")], "");
418 let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
419
420 assert!(bundle.get_module(0).is_some());
421 assert!(bundle.get_module(1).is_none());
422 assert!(bundle.get_module(999).is_none());
423 }
424
425 #[test]
426 fn test_invalid_magic() {
427 let mut data = make_test_bundle(&[], "");
428 data[0] = 0x00;
430 let err = IndexedRamBundle::from_bytes(&data).unwrap_err();
431 assert!(matches!(err, RamBundleError::InvalidMagic));
432 }
433
434 #[test]
435 fn test_too_short_header() {
436 let err = IndexedRamBundle::from_bytes(&[0xE5, 0xD1, 0x0B, 0xFB]).unwrap_err();
437 assert!(matches!(err, RamBundleError::TooShort));
438 }
439
440 #[test]
441 fn test_too_short_for_table() {
442 let mut data = Vec::new();
444 data.extend_from_slice(&RAM_BUNDLE_MAGIC.to_le_bytes());
445 data.extend_from_slice(&1000_u32.to_le_bytes());
446 data.extend_from_slice(&0_u32.to_le_bytes());
447 let err = IndexedRamBundle::from_bytes(&data).unwrap_err();
448 assert!(matches!(err, RamBundleError::TooShort));
449 }
450
451 #[test]
452 fn test_module_extends_beyond_data() {
453 let data = make_test_bundle(&[Some("hello world")], "");
455 let truncated = &data[..data.len() - 5];
456 let err = IndexedRamBundle::from_bytes(truncated).unwrap_err();
457 assert!(matches!(err, RamBundleError::InvalidEntry(_)));
458 }
459
460 #[test]
461 fn test_module_iteration_order() {
462 let modules = vec![Some("mod0"), None, Some("mod2"), None, Some("mod4")];
463 let data = make_test_bundle(&modules, "");
464 let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
465
466 let ids: Vec<u32> = bundle.modules().map(|m| m.id).collect();
467 assert_eq!(ids, vec![0, 2, 4]);
468 }
469
470 #[test]
471 fn test_is_unbundle_dir_nonexistent() {
472 assert!(!is_unbundle_dir(Path::new("/nonexistent/path")));
473 }
474
475 #[test]
476 fn test_display_errors() {
477 assert_eq!(
478 RamBundleError::InvalidMagic.to_string(),
479 "invalid RAM bundle magic number"
480 );
481 assert_eq!(
482 RamBundleError::TooShort.to_string(),
483 "data too short for RAM bundle header"
484 );
485 assert_eq!(
486 RamBundleError::InvalidEntry("bad".to_string()).to_string(),
487 "invalid module entry: bad"
488 );
489 }
490
491 #[test]
492 fn test_ram_bundle_type_equality() {
493 assert_eq!(RamBundleType::Indexed, RamBundleType::Indexed);
494 assert_eq!(RamBundleType::Unbundle, RamBundleType::Unbundle);
495 assert_ne!(RamBundleType::Indexed, RamBundleType::Unbundle);
496 }
497}