From 372901db2a9c1b1a10ac74e677554b82e6dd73c1 Mon Sep 17 00:00:00 2001 From: Seokju Na Date: Sun, 29 Sep 2024 13:02:43 +0900 Subject: [PATCH 1/4] add `staged_fixed` option --- lefthook.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/lefthook.yml b/lefthook.yml index 545178e..7d013e0 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -8,6 +8,7 @@ pre-commit: format:taplo: glob: "*.toml" run: taplo fmt --colors=never {staged_files} + stage_fixed: true format:biome: glob: "*.{js,ts,jsx,tsx,cjs,mjs,cts,mts}" run: yarn biome format --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} From d00e66a8a9a7ea042249036b1b9a08ac44da1583 Mon Sep 17 00:00:00 2001 From: Seokju Na Date: Sun, 29 Sep 2024 13:02:53 +0900 Subject: [PATCH 2/4] fix rust test script --- justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/justfile b/justfile index 33f89c6..cc01811 100644 --- a/justfile +++ b/justfile @@ -12,7 +12,7 @@ test: yarn vitest run test-rust: - cargo test run --workspace --no-fail-fast + cargo test --workspace biome: yarn biome check From 0fee4cb73fe40f56b06a39de8c806e045e3832da Mon Sep 17 00:00:00 2001 From: Seokju Na Date: Sun, 29 Sep 2024 13:03:18 +0900 Subject: [PATCH 3/4] add core crate --- Cargo.toml | 7 ++ crates/webview-bundle/Cargo.toml | 9 ++ crates/webview-bundle/src/builder.rs | 64 ++++++++++++++ crates/webview-bundle/src/bundle.rs | 119 +++++++++++++++++++++++++++ crates/webview-bundle/src/decoder.rs | 87 ++++++++++++++++++++ crates/webview-bundle/src/encoder.rs | 76 +++++++++++++++++ crates/webview-bundle/src/error.rs | 19 +++++ crates/webview-bundle/src/lib.rs | 13 ++- 8 files changed, 393 insertions(+), 1 deletion(-) create mode 100644 crates/webview-bundle/src/builder.rs create mode 100644 crates/webview-bundle/src/bundle.rs create mode 100644 crates/webview-bundle/src/decoder.rs create mode 100644 crates/webview-bundle/src/encoder.rs create mode 100644 crates/webview-bundle/src/error.rs diff --git a/Cargo.toml b/Cargo.toml index a48c9d9..176e262 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/webview-bundle/Cargo.toml b/crates/webview-bundle/Cargo.toml index 0c623d2..3892e1c 100644 --- a/crates/webview-bundle/Cargo.toml +++ b/crates/webview-bundle/Cargo.toml @@ -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 } diff --git a/crates/webview-bundle/src/builder.rs b/crates/webview-bundle/src/builder.rs new file mode 100644 index 0000000..96c1ff8 --- /dev/null +++ b/crates/webview-bundle/src/builder.rs @@ -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, + offset: u64, + descriptors: Vec, + data: Vec, +} + +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>(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"); + } +} diff --git a/crates/webview-bundle/src/bundle.rs b/crates/webview-bundle/src/bundle.rs new file mode 100644 index 0000000..ad4c26a --- /dev/null +++ b/crates/webview-bundle/src/bundle.rs @@ -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>(&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, + pub(crate) data: Vec, +} + +impl Bundle { + pub fn version(&self) -> &Version { + &self.version + } + + pub fn read_file>(&self, path: P) -> crate::Result> { + 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>(&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 ( +
+

Count: {count}

+ +
+ ); +} + "#; + 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#"

Hello World

"#; + 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, + )); + } +} diff --git a/crates/webview-bundle/src/decoder.rs b/crates/webview-bundle/src/decoder.rs new file mode 100644 index 0000000..fd396bc --- /dev/null +++ b/crates/webview-bundle/src/decoder.rs @@ -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 { + Decoder::new(&buf).decode() +} + +struct Decoder { + c: Cursor, +} + +impl Decoder { + fn new(buf: T) -> Self { + Self { + c: Cursor::new(buf), + } + } +} + +impl> Decoder { + fn decode(&mut self) -> crate::Result { + // 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 { + 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> { + 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, _) = + 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); + } +} diff --git a/crates/webview-bundle/src/encoder.rs b/crates/webview-bundle/src/encoder.rs new file mode 100644 index 0000000..08207ae --- /dev/null +++ b/crates/webview-bundle/src/encoder.rs @@ -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(bundle: &Bundle, write: W) -> crate::Result<()> { + Encoder::new(write).encode(bundle)?; + Ok(()) +} + +pub fn encode_bytes(bundle: &Bundle) -> crate::Result> { + let mut write = Vec::new(); + encode(bundle, &mut write)?; + Ok(write) +} + +struct Encoder { + w: W, +} + +impl Encoder { + 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) -> crate::Result<()> { + let mut encoded: Vec = 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 + ] + ); + } +} diff --git a/crates/webview-bundle/src/error.rs b/crates/webview-bundle/src/error.rs new file mode 100644 index 0000000..ca56c9d --- /dev/null +++ b/crates/webview-bundle/src/error.rs @@ -0,0 +1,19 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Compress(#[from] lz4_flex::block::CompressError), + #[error(transparent)] + Decompress(#[from] lz4_flex::block::DecompressError), + #[error(transparent)] + Encode(#[from] bincode::error::EncodeError), + #[error(transparent)] + Decode(#[from] bincode::error::DecodeError), + #[error("header magic mismatch")] + InvalidMagic, + #[error("invalid version format")] + InvalidVersion, + #[error("file not found")] + FileNotFound, +} diff --git a/crates/webview-bundle/src/lib.rs b/crates/webview-bundle/src/lib.rs index da0f5d9..823b0d4 100644 --- a/crates/webview-bundle/src/lib.rs +++ b/crates/webview-bundle/src/lib.rs @@ -1 +1,12 @@ -pub fn main() {} +mod builder; +mod bundle; +mod decoder; +mod encoder; +mod error; + +pub(crate) type Result = std::result::Result; + +pub use bundle::{Bundle, Version}; +pub use decoder::decode; +pub use encoder::encode; +pub use error::Error; From 50ac4b252e084388846331da9e9d83901b217e5a Mon Sep 17 00:00:00 2001 From: Seokju Na Date: Sun, 29 Sep 2024 13:15:45 +0900 Subject: [PATCH 4/4] hold --- .github/workflows/pull_request.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index af3f03d..0e5af20 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -36,7 +36,7 @@ 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]')" @@ -44,7 +44,7 @@ jobs: - 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]')"