1use crate::builder::Builder;
2use bincode::{Decode, Encode};
3use lz4_flex::decompress_size_prepended;
4use std::fmt::{Display, Formatter};
5use std::io::{Cursor, Read};
6use std::path::Path;
7
8pub const HEADER_MAGIC_BYTES: [u8; 8] = [0xf0, 0x9f, 0x8c, 0x90, 0xf0, 0x9f, 0x8e, 0x81];
10pub(crate) const VERSION_BYTES_LENGTH: usize = 4;
11pub(crate) const FILE_DESCRIPTORS_SIZE_BYTES_LENGTH: usize = 4;
12
13#[derive(Debug, PartialEq, Eq, Copy, Clone)]
14pub enum Version {
15 Version1,
17}
18
19impl Default for Version {
20 fn default() -> Self {
21 Self::Version1
22 }
23}
24
25impl Version {
26 pub fn bytes(&self) -> &[u8; 4] {
27 match self {
28 Version::Version1 => &[0x76, 0x31, 0, 0],
29 }
30 }
31}
32
33impl Display for Version {
34 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
35 let s = match self {
36 Self::Version1 => "version1",
37 };
38 f.write_str(s)
39 }
40}
41
42#[derive(Debug, PartialEq, Encode, Decode, Clone)]
43pub struct FileDescriptor {
44 pub(crate) path: String,
45 pub(crate) offset: u64,
46 pub(crate) length: u64,
47}
48
49impl FileDescriptor {
50 pub(crate) fn path_matches<P: AsRef<Path>>(&self, path: &P) -> bool {
51 self.path == path.as_ref().to_string_lossy()
52 }
53
54 pub fn path(&self) -> &String {
55 &self.path
56 }
57
58 pub fn size(&self) -> u64 {
59 self.length
60 }
61}
62
63#[derive(Debug, PartialEq, Clone)]
64pub struct Bundle {
65 pub(crate) version: Version,
66 pub(crate) descriptors: Vec<FileDescriptor>,
67 pub(crate) data: Vec<u8>,
68}
69
70impl Bundle {
71 pub fn version(&self) -> &Version {
72 &self.version
73 }
74
75 pub fn descriptors(&self) -> &[FileDescriptor] {
76 &self.descriptors
77 }
78
79 pub fn read_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<Vec<u8>> {
80 let descriptor = self
81 .find_descriptor(path)
82 .ok_or(crate::Error::FileNotFound)?;
83 let mut cursor = Cursor::new(&self.data);
84 cursor.set_position(descriptor.offset);
85 let mut buf = vec![0; descriptor.length as usize];
86 cursor.read_exact(&mut buf)?;
87 let file = decompress_size_prepended(&buf)?;
88 Ok(file)
89 }
90
91 pub fn builder() -> Builder {
92 Builder::new()
93 }
94
95 fn find_descriptor<P: AsRef<Path>>(&self, path: P) -> Option<&FileDescriptor> {
96 self.descriptors.iter().find(|x| x.path_matches(&path))
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn read_file() {
106 let path = Path::new("index.jsx");
107 let file = r#"
108import React, { useState } from 'react';
109
110export function MyComponent() {
111 const [count, setCount] = useState(0);
112 return (
113 <div>
114 <h1>Count: {count}</h1>
115 <button onClick={() => setCount(x => x + 1)}>increse</button>
116 </div>
117 );
118}
119 "#;
120 let bundle = Bundle::builder().add_file(path, file.as_bytes()).build();
121 assert_eq!(bundle.read_file(path).unwrap(), file.as_bytes());
122 }
123
124 #[test]
125 fn read_file_err() {
126 let path1 = Path::new("index.html");
127 let file1 = r#"<h1>Hello World</h1>"#;
128 let path2 = Path::new("index.js");
129 let file2 = r#"const a = 10;"#;
130 let bundle = Bundle::builder()
131 .add_file(path1, file1.as_bytes())
132 .add_file(path2, file2.as_bytes())
133 .build();
134 assert!(bundle.read_file(path1).is_ok());
135 assert!(bundle.read_file(path2).is_ok());
136 assert!(matches!(
137 bundle.read_file(Path::new("other.js")).unwrap_err(),
138 crate::Error::FileNotFound,
139 ));
140 }
141}