1use std::fmt;
41use std::path::Path;
42
43const RAM_BUNDLE_MAGIC: u32 = 0xFB0B_D1E5;
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.checked_add(table_size).ok_or(RamBundleError::TooShort)?;
160
161 if data.len() < table_end {
162 return Err(RamBundleError::TooShort);
163 }
164
165 let startup_start = table_end;
167 let startup_end =
168 startup_start.checked_add(startup_code_size).ok_or(RamBundleError::TooShort)?;
169
170 if data.len() < startup_end {
171 return Err(RamBundleError::TooShort);
172 }
173
174 let startup_code = std::str::from_utf8(&data[startup_start..startup_end])
175 .map_err(|e| {
176 RamBundleError::InvalidEntry(format!("startup code is not valid UTF-8: {e}"))
177 })?
178 .to_owned();
179
180 let modules_base = startup_end;
182
183 let mut modules = Vec::with_capacity(module_count as usize);
184
185 for i in 0..module_count as usize {
186 let entry_offset = HEADER_SIZE + i * MODULE_ENTRY_SIZE;
187 let offset = read_u32_le(data, entry_offset).unwrap() as usize;
188 let length = read_u32_le(data, entry_offset + 4).unwrap() as usize;
189
190 if offset == 0 && length == 0 {
191 modules.push(None);
192 continue;
193 }
194
195 let abs_start = modules_base.checked_add(offset).ok_or_else(|| {
196 RamBundleError::InvalidEntry(format!("module {i} offset overflows"))
197 })?;
198 let abs_end = abs_start.checked_add(length).ok_or_else(|| {
199 RamBundleError::InvalidEntry(format!("module {i} length overflows"))
200 })?;
201
202 if abs_end > data.len() {
203 return Err(RamBundleError::InvalidEntry(format!(
204 "module {i} extends beyond data (offset={offset}, length={length}, data_len={})",
205 data.len()
206 )));
207 }
208
209 let source_code = std::str::from_utf8(&data[abs_start..abs_end])
210 .map_err(|e| {
211 RamBundleError::InvalidEntry(format!(
212 "module {i} source is not valid UTF-8: {e}"
213 ))
214 })?
215 .to_owned();
216
217 modules.push(Some(RamBundleModule { id: i as u32, source_code }));
218 }
219
220 Ok(Self { module_count, startup_code, modules })
221 }
222
223 pub fn module_count(&self) -> u32 {
225 self.module_count
226 }
227
228 pub fn get_module(&self, id: u32) -> Option<&RamBundleModule> {
230 self.modules.get(id as usize)?.as_ref()
231 }
232
233 pub fn modules(&self) -> impl Iterator<Item = &RamBundleModule> {
235 self.modules.iter().filter_map(|m| m.as_ref())
236 }
237
238 pub fn startup_code(&self) -> &str {
240 &self.startup_code
241 }
242}
243
244pub fn is_ram_bundle(data: &[u8]) -> bool {
248 read_u32_le(data, 0) == Some(RAM_BUNDLE_MAGIC)
249}
250
251pub fn is_unbundle_dir(path: &Path) -> bool {
255 path.join("js-modules").is_dir()
256}
257
258fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
260 if offset + 4 > data.len() {
261 return None;
262 }
263 Some(u32::from_le_bytes([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]))
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 fn make_test_bundle(modules: &[Option<&str>], startup: &str) -> Vec<u8> {
275 let mut data = Vec::new();
276
277 data.extend_from_slice(&RAM_BUNDLE_MAGIC.to_le_bytes());
279 data.extend_from_slice(&(modules.len() as u32).to_le_bytes());
280 data.extend_from_slice(&(startup.len() as u32).to_le_bytes());
281
282 let mut module_bodies: Vec<(u32, u32)> = Vec::new();
286 let mut current_offset: u32 = 0;
287
288 for module in modules {
289 match module {
290 Some(src) => {
291 let len = src.len() as u32;
292 module_bodies.push((current_offset, len));
293 current_offset += len;
294 }
295 None => {
296 module_bodies.push((0, 0));
297 }
298 }
299 }
300
301 for &(offset, length) in &module_bodies {
303 data.extend_from_slice(&offset.to_le_bytes());
304 data.extend_from_slice(&length.to_le_bytes());
305 }
306
307 data.extend_from_slice(startup.as_bytes());
309
310 for module in modules.iter().flatten() {
312 data.extend_from_slice(module.as_bytes());
313 }
314
315 data
316 }
317
318 #[test]
319 fn test_is_ram_bundle() {
320 let data = make_test_bundle(&[], "");
321 assert!(is_ram_bundle(&data));
322 }
323
324 #[test]
325 fn test_is_ram_bundle_wrong_magic() {
326 let data = [0x00, 0x00, 0x00, 0x00];
327 assert!(!is_ram_bundle(&data));
328 }
329
330 #[test]
331 fn test_is_ram_bundle_too_short() {
332 assert!(!is_ram_bundle(&[0xE5, 0xD1, 0x0B]));
333 assert!(!is_ram_bundle(&[]));
334 }
335
336 #[test]
337 fn test_parse_empty_bundle() {
338 let data = make_test_bundle(&[], "var x = 1;");
339 let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
340 assert_eq!(bundle.module_count(), 0);
341 assert_eq!(bundle.startup_code(), "var x = 1;");
342 assert_eq!(bundle.modules().count(), 0);
343 }
344
345 #[test]
346 fn test_parse_single_module() {
347 let data = make_test_bundle(&[Some("__d(function(){},0);")], "startup();");
348 let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
349
350 assert_eq!(bundle.module_count(), 1);
351 assert_eq!(bundle.startup_code(), "startup();");
352
353 let module = bundle.get_module(0).unwrap();
354 assert_eq!(module.id, 0);
355 assert_eq!(module.source_code, "__d(function(){},0);");
356 }
357
358 #[test]
359 fn test_parse_multiple_modules() {
360 let modules = vec![
361 Some("__d(function(){console.log('a')},0);"),
362 Some("__d(function(){console.log('b')},1);"),
363 Some("__d(function(){console.log('c')},2);"),
364 ];
365 let data = make_test_bundle(&modules, "require(0);");
366 let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
367
368 assert_eq!(bundle.module_count(), 3);
369 assert_eq!(bundle.startup_code(), "require(0);");
370
371 for (i, module) in bundle.modules().enumerate() {
372 assert_eq!(module.id, i as u32);
373 assert!(module.source_code.contains(&format!("'{}'", (b'a' + i as u8) as char)));
374 }
375 }
376
377 #[test]
378 fn test_empty_module_slots() {
379 let modules = vec![Some("__d(function(){},0);"), None, Some("__d(function(){},2);")];
380 let data = make_test_bundle(&modules, "");
381 let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
382
383 assert_eq!(bundle.module_count(), 3);
384 assert!(bundle.get_module(0).is_some());
385 assert!(bundle.get_module(1).is_none());
386 assert!(bundle.get_module(2).is_some());
387
388 assert_eq!(bundle.modules().count(), 2);
390 }
391
392 #[test]
393 fn test_get_module_out_of_range() {
394 let data = make_test_bundle(&[Some("__d(function(){},0);")], "");
395 let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
396
397 assert!(bundle.get_module(0).is_some());
398 assert!(bundle.get_module(1).is_none());
399 assert!(bundle.get_module(999).is_none());
400 }
401
402 #[test]
403 fn test_invalid_magic() {
404 let mut data = make_test_bundle(&[], "");
405 data[0] = 0x00;
407 let err = IndexedRamBundle::from_bytes(&data).unwrap_err();
408 assert!(matches!(err, RamBundleError::InvalidMagic));
409 }
410
411 #[test]
412 fn test_too_short_header() {
413 let err = IndexedRamBundle::from_bytes(&[0xE5, 0xD1, 0x0B, 0xFB]).unwrap_err();
414 assert!(matches!(err, RamBundleError::TooShort));
415 }
416
417 #[test]
418 fn test_too_short_for_table() {
419 let mut data = Vec::new();
421 data.extend_from_slice(&RAM_BUNDLE_MAGIC.to_le_bytes());
422 data.extend_from_slice(&1000_u32.to_le_bytes());
423 data.extend_from_slice(&0_u32.to_le_bytes());
424 let err = IndexedRamBundle::from_bytes(&data).unwrap_err();
425 assert!(matches!(err, RamBundleError::TooShort));
426 }
427
428 #[test]
429 fn test_module_extends_beyond_data() {
430 let data = make_test_bundle(&[Some("hello world")], "");
432 let truncated = &data[..data.len() - 5];
433 let err = IndexedRamBundle::from_bytes(truncated).unwrap_err();
434 assert!(matches!(err, RamBundleError::InvalidEntry(_)));
435 }
436
437 #[test]
438 fn test_module_iteration_order() {
439 let modules = vec![Some("mod0"), None, Some("mod2"), None, Some("mod4")];
440 let data = make_test_bundle(&modules, "");
441 let bundle = IndexedRamBundle::from_bytes(&data).unwrap();
442
443 let ids: Vec<u32> = bundle.modules().map(|m| m.id).collect();
444 assert_eq!(ids, vec![0, 2, 4]);
445 }
446
447 #[test]
448 fn test_is_unbundle_dir_nonexistent() {
449 assert!(!is_unbundle_dir(Path::new("/nonexistent/path")));
450 }
451
452 #[test]
453 fn test_display_errors() {
454 assert_eq!(RamBundleError::InvalidMagic.to_string(), "invalid RAM bundle magic number");
455 assert_eq!(RamBundleError::TooShort.to_string(), "data too short for RAM bundle header");
456 assert_eq!(
457 RamBundleError::InvalidEntry("bad".to_string()).to_string(),
458 "invalid module entry: bad"
459 );
460 }
461
462 #[test]
463 fn test_ram_bundle_type_equality() {
464 assert_eq!(RamBundleType::Indexed, RamBundleType::Indexed);
465 assert_eq!(RamBundleType::Unbundle, RamBundleType::Unbundle);
466 assert_ne!(RamBundleType::Indexed, RamBundleType::Unbundle);
467 }
468}