1use crate::builder::Builder;
2use bincode::de::Decoder;
3use bincode::enc::Encoder;
4use bincode::error::{DecodeError, EncodeError};
5use bincode::{Decode, Encode};
6use lz4_flex::decompress_size_prepended;
7use std::collections::HashMap;
8use std::fmt::{Display, Formatter};
9use std::io::{Cursor, Read};
10use std::ops::{Deref, DerefMut};
11
12pub const HEADER_MAGIC_BYTES: [u8; 8] = [0xf0, 0x9f, 0x8c, 0x90, 0xf0, 0x9f, 0x8e, 0x81];
14pub(crate) const VERSION_BYTES_LENGTH: usize = 1;
15pub(crate) const FILE_DESCRIPTORS_SIZE_BYTES_LENGTH: usize = 4;
16
17#[derive(Debug, PartialEq, Eq, Copy, Clone)]
18pub enum Version {
19 Version1,
21}
22
23impl Default for Version {
24 fn default() -> Self {
25 Self::Version1
26 }
27}
28
29impl Version {
30 pub const fn bytes(&self) -> [u8; 1] {
31 match self {
32 Version::Version1 => [0x01],
33 }
34 }
35}
36
37impl Display for Version {
38 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
39 let s = match self {
40 Self::Version1 => "v1",
41 };
42 f.write_str(s)
43 }
44}
45
46#[derive(Debug, PartialEq, Clone)]
47pub struct FileDescriptorData {
48 pub offset: u32,
49 pub length: u32,
50}
51
52impl FileDescriptorData {
53 pub fn new(offset: u32, length: u32) -> Self {
54 Self { offset, length }
55 }
56}
57
58#[derive(Debug, Default, PartialEq, Clone)]
59pub struct FileDescriptors(pub(crate) HashMap<String, FileDescriptorData>);
60
61impl Encode for FileDescriptors {
62 fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
63 let data = self
64 .0
65 .iter()
66 .map(|(path, data)| (path, (data.offset, data.length)))
67 .collect::<HashMap<_, _>>();
68 Encode::encode(&data, encoder)?;
69 Ok(())
70 }
71}
72
73impl<T> Decode<T> for FileDescriptors {
74 fn decode<D: Decoder>(decoder: &mut D) -> Result<Self, DecodeError> {
75 let data = HashMap::<String, (u32, u32)>::decode(decoder)?
76 .into_iter()
77 .map(|(path, (offset, length))| (path, FileDescriptorData { offset, length }))
78 .collect::<HashMap<_, _>>();
79 Ok(Self(data))
80 }
81}
82
83impl Deref for FileDescriptors {
84 type Target = HashMap<String, FileDescriptorData>;
85
86 fn deref(&self) -> &Self::Target {
87 &self.0
88 }
89}
90
91impl DerefMut for FileDescriptors {
92 fn deref_mut(&mut self) -> &mut Self::Target {
93 &mut self.0
94 }
95}
96
97impl FileDescriptors {
98 pub fn get(&self, path: &str) -> Option<&FileDescriptorData> {
99 self.deref().get(path)
100 }
101}
102
103#[derive(Debug, PartialEq, Clone)]
104pub struct Bundle {
105 pub(crate) version: Version,
106 pub(crate) descriptors: FileDescriptors,
107 pub(crate) data: Vec<u8>,
108}
109
110impl Bundle {
111 pub fn version(&self) -> &Version {
112 &self.version
113 }
114
115 pub fn descriptors(&self) -> &FileDescriptors {
116 &self.descriptors
117 }
118
119 pub fn read_file(&self, path: &str) -> crate::Result<Vec<u8>> {
120 let &FileDescriptorData { offset, length } = self
121 .descriptors
122 .get(path)
123 .ok_or(crate::Error::FileNotFound)?;
124 let mut cursor = Cursor::new(&self.data);
125 cursor.set_position(offset.into());
126 let mut buf = vec![0; length as usize];
127 cursor.read_exact(&mut buf)?;
128 let file = decompress_size_prepended(&buf)?;
129 Ok(file)
130 }
131
132 pub fn builder() -> Builder {
133 Builder::new()
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn read_file() {
143 let file = r#"
144import React, { useState } from 'react';
145
146export function MyComponent() {
147 const [count, setCount] = useState(0);
148 return (
149 <div>
150 <h1>Count: {count}</h1>
151 <button onClick={() => setCount(x => x + 1)}>increse</button>
152 </div>
153 );
154}
155 "#;
156 let bundle = Bundle::builder()
157 .add_file("index.jsx", file.as_bytes())
158 .build();
159 assert_eq!(bundle.read_file("index.jsx").unwrap(), file.as_bytes());
160 }
161
162 #[test]
163 fn read_file_err() {
164 let file1 = r#"<h1>Hello World</h1>"#;
165 let file2 = r#"const a = 10;"#;
166 let bundle = Bundle::builder()
167 .add_file("index.html", file1.as_bytes())
168 .add_file("index.js", file2.as_bytes())
169 .build();
170 assert!(bundle.read_file("index.html").is_ok());
171 assert!(bundle.read_file("index.js").is_ok());
172 assert!(matches!(
173 bundle.read_file("not_exists.js").unwrap_err(),
174 crate::Error::FileNotFound,
175 ));
176 }
177}