webview_bundle/
bundle.rs

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
12// 🌐🎁
13pub 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  /// Version 1
20  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}