webview_bundle/
bundle.rs

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
8// 🌐🎁
9pub 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  /// Version 1
16  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}