Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement webview_bundle crate #2

Merged
merged 4 commits into from
Sep 29, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pull_request.yaml
Original file line number Diff line number Diff line change
@@ -36,15 +36,15 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/node-setup
- uses: ./.github/actions/yarn-setup
- run: yarn tsc --noEmit
# - run: yarn tsc --noEmit
test:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/node-setup
- uses: ./.github/actions/yarn-setup
- run: yarn vitest run
# - run: yarn vitest run
lint-rust:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
[workspace]
members = ["crates/*"]
resolver = "2"

[workspace.dependencies]
bincode = "^2.0.0-rc.3"
lz4_flex = "^0.11.3"
serde = { version = "^1", features = ["derive"] }
serde_json = "^1"
thiserror = "^1"
9 changes: 9 additions & 0 deletions crates/webview-bundle/Cargo.toml
Original file line number Diff line number Diff line change
@@ -6,3 +6,12 @@ license = "MIT"
name = "webview-bundle"
repository = "https://github.com/seokju-na/webview-bundle"
version = "0.0.0"

[lib]
bench = false
doctest = false

[dependencies]
bincode = { workspace = true }
lz4_flex = { workspace = true }
thiserror = { workspace = true }
64 changes: 64 additions & 0 deletions crates/webview-bundle/src/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use crate::bundle::FileDescriptor;
use crate::{Bundle, Version};
use lz4_flex::compress_prepend_size;
use std::path::Path;

#[derive(Default)]
pub struct Builder {
version: Option<Version>,
offset: u64,
descriptors: Vec<FileDescriptor>,
data: Vec<u8>,
}

impl Builder {
pub(crate) fn new() -> Self {
Default::default()
}

pub fn version(mut self, version: Version) -> Self {
self.version = Some(version);
self
}

pub fn add_file<P: AsRef<Path>>(mut self, path: P, data: &[u8]) -> Self {
let compressed = compress_prepend_size(data);
let length = compressed.len() as u64;
let descriptor = FileDescriptor {
path: path.as_ref().to_string_lossy().to_string(),
offset: self.offset,
length,
};
self.offset += length;
self.descriptors.push(descriptor);
self.data.extend_from_slice(&compressed);
self
}

pub fn build(self) -> Bundle {
let version = self.version.unwrap_or_default();
Bundle {
version,
descriptors: self.descriptors,
data: self.data,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn build() {
let path = Path::new("index.js");
let data = r#"
const a = 10;
export a;
"#;
let bundle = Builder::new().add_file(path, data.as_bytes()).build();
assert_eq!(bundle.version(), &Version::Version1);
assert_eq!(bundle.descriptors.len(), 1);
assert_eq!(bundle.descriptors.first().unwrap().path, "index.js");
}
}
119 changes: 119 additions & 0 deletions crates/webview-bundle/src/bundle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use crate::builder::Builder;
use bincode::{Decode, Encode};
use lz4_flex::decompress_size_prepended;
use std::io::{Cursor, Read};
use std::path::Path;

// 🌐🎁
pub const HEADER_MAGIC_BYTES: [u8; 8] = [0xf0, 0x9f, 0x8c, 0x90, 0xf0, 0x9f, 0x8e, 0x81];
pub(crate) const VERSION_BYTES_LENGTH: usize = 4;
pub(crate) const FILE_DESCRIPTORS_SIZE_BYTES_LENGTH: usize = 4;

#[derive(Debug, PartialEq, Eq)]
pub enum Version {
/// Version 1
Version1,
}

impl Default for Version {
fn default() -> Self {
Self::Version1
}
}

impl Version {
pub fn bytes(&self) -> &[u8; 4] {
match self {
Version::Version1 => &[0x76, 0x31, 0, 0],
}
}
}

#[derive(Debug, PartialEq, Encode, Decode)]
pub struct FileDescriptor {
pub(crate) path: String,
pub(crate) offset: u64,
pub(crate) length: u64,
}

impl FileDescriptor {
pub(crate) fn path_matches<P: AsRef<Path>>(&self, path: &P) -> bool {
self.path == path.as_ref().to_string_lossy()
}
}

#[derive(Debug, PartialEq)]
pub struct Bundle {
pub(crate) version: Version,
pub(crate) descriptors: Vec<FileDescriptor>,
pub(crate) data: Vec<u8>,
}

impl Bundle {
pub fn version(&self) -> &Version {
&self.version
}

pub fn read_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<Vec<u8>> {
let descriptor = self
.find_descriptor(path)
.ok_or(crate::Error::FileNotFound)?;
let mut cursor = Cursor::new(&self.data);
cursor.set_position(descriptor.offset);
let mut buf = vec![0; descriptor.length as usize];
cursor.read_exact(&mut buf)?;
let file = decompress_size_prepended(&buf)?;
Ok(file)
}

pub fn builder() -> Builder {
Builder::new()
}

fn find_descriptor<P: AsRef<Path>>(&self, path: P) -> Option<&FileDescriptor> {
self.descriptors.iter().find(|x| x.path_matches(&path))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn read_file() {
let path = Path::new("index.jsx");
let file = r#"
import React, { useState } from 'react';

export function MyComponent() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(x => x + 1)}>increse</button>
</div>
);
}
"#;
let bundle = Bundle::builder().add_file(path, file.as_bytes()).build();
assert_eq!(bundle.read_file(path).unwrap(), file.as_bytes());
}

#[test]
fn read_file_err() {
let path1 = Path::new("index.html");
let file1 = r#"<h1>Hello World</h1>"#;
let path2 = Path::new("index.js");
let file2 = r#"const a = 10;"#;
let bundle = Bundle::builder()
.add_file(path1, file1.as_bytes())
.add_file(path2, file2.as_bytes())
.build();
assert!(bundle.read_file(path1).is_ok());
assert!(bundle.read_file(path2).is_ok());
assert!(matches!(
bundle.read_file(Path::new("other.js")).unwrap_err(),
crate::Error::FileNotFound,
));
}
}
87 changes: 87 additions & 0 deletions crates/webview-bundle/src/decoder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use crate::bundle::{
Bundle, FileDescriptor, Version, FILE_DESCRIPTORS_SIZE_BYTES_LENGTH, HEADER_MAGIC_BYTES,
VERSION_BYTES_LENGTH,
};
use bincode::{config, decode_from_slice};
use std::io::{Cursor, Read};

pub fn decode(buf: impl AsRef<[u8]>) -> crate::Result<Bundle> {
Decoder::new(&buf).decode()
}

struct Decoder<T> {
c: Cursor<T>,
}

impl<T> Decoder<T> {
fn new(buf: T) -> Self {
Self {
c: Cursor::new(buf),
}
}
}

impl<T: AsRef<[u8]>> Decoder<T> {
fn decode(&mut self) -> crate::Result<Bundle> {
// TODO: check checksum?
self.read_magic_bytes()?;
let version = self.read_version()?;
let descriptors = self.read_file_descriptors()?;
let mut data = Vec::new();
self.c.read_to_end(&mut data)?;
let bundle = Bundle {
version,
descriptors,
data,
};
Ok(bundle)
}

fn read_magic_bytes(&mut self) -> crate::Result<()> {
let mut buf = [0; HEADER_MAGIC_BYTES.len()];
self.c.read_exact(&mut buf)?;
if buf != HEADER_MAGIC_BYTES {
return Err(crate::Error::InvalidMagic);
}
Ok(())
}

fn read_version(&mut self) -> crate::Result<Version> {
let mut buf = [0; VERSION_BYTES_LENGTH];
self.c.read_exact(&mut buf)?;
if &buf == Version::Version1.bytes() {
return Ok(Version::Version1);
}
Err(crate::Error::InvalidVersion)
}

fn read_file_descriptors(&mut self) -> crate::Result<Vec<FileDescriptor>> {
let mut size_buf = [0; FILE_DESCRIPTORS_SIZE_BYTES_LENGTH];
self.c.read_exact(&mut size_buf)?;
let size = u32::from_be_bytes(AsRef::<[u8]>::as_ref(&size_buf).try_into().unwrap());

let mut descriptors_buf = vec![0; size as usize];
self.c.read_exact(&mut descriptors_buf)?;
let config = config::standard().with_big_endian();
let (file_descriptors, _): (Vec<FileDescriptor>, _) =
decode_from_slice(&descriptors_buf, config)?;
Ok(file_descriptors)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::encoder::encode_bytes;
use std::path::Path;

#[test]
fn encode_and_decode() {
let path = Path::new("index.js");
let file = r#"const a = 10;"#;
let bundle = Bundle::builder().add_file(path, file.as_bytes()).build();
let encoded = encode_bytes(&bundle).unwrap();
let decoded = decode(encoded).unwrap();
assert_eq!(bundle, decoded);
}
}
76 changes: 76 additions & 0 deletions crates/webview-bundle/src/encoder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use crate::bundle::{Bundle, FileDescriptor, Version, HEADER_MAGIC_BYTES};
use bincode::{config, encode_to_vec};
use std::io::Write;

pub fn encode<W: Write>(bundle: &Bundle, write: W) -> crate::Result<()> {
Encoder::new(write).encode(bundle)?;
Ok(())
}

pub fn encode_bytes(bundle: &Bundle) -> crate::Result<Vec<u8>> {
let mut write = Vec::new();
encode(bundle, &mut write)?;
Ok(write)
}

struct Encoder<W: Write> {
w: W,
}

impl<W: Write> Encoder<W> {
fn new(w: W) -> Self {
Self { w }
}

fn encode(&mut self, bundle: &Bundle) -> crate::Result<()> {
self.write_magic()?;
self.write_version(&bundle.version)?;
self.write_file_descriptors(&bundle.descriptors)?;
self.w.write_all(&bundle.data)?;
Ok(())
}

fn write_magic(&mut self) -> crate::Result<()> {
self.w.write_all(&HEADER_MAGIC_BYTES)?;
Ok(())
}

fn write_version(&mut self, version: &Version) -> crate::Result<()> {
self.w.write_all(version.bytes())?;
Ok(())
}

fn write_file_descriptors(&mut self, descriptors: &Vec<FileDescriptor>) -> crate::Result<()> {
let mut encoded: Vec<u8> = vec![];
let config = config::standard().with_big_endian();
let bytes = encode_to_vec(descriptors, config)?;
let bytes_len = (bytes.len() as u32).to_be_bytes();
encoded.extend_from_slice(&bytes_len);
encoded.extend_from_slice(&bytes);
self.w.write_all(&encoded)?;
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;

#[test]
fn encode_ok() {
let path = Path::new("index.js");
let file = r#"const a = 10;"#;
let bundle = Bundle::builder().add_file(path, file.as_bytes()).build();
let mut write = Vec::new();
encode(&bundle, &mut write).unwrap();
assert_eq!(
write,
[
240, 159, 140, 144, 240, 159, 142, 129, 118, 49, 0, 0, 0, 0, 0, 12, 1, 8, 105, 110, 100,
101, 120, 46, 106, 115, 0, 18, 13, 0, 0, 0, 208, 99, 111, 110, 115, 116, 32, 97, 32, 61,
32, 49, 48, 59
]
);
}
}
Loading