diff --git a/.gitignore b/.gitignore index ea8c4bf..d744ce2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target + +Cargo.lock diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 9922337..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "const-macros" -version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index c38dd5a..7139239 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,10 +9,8 @@ repository = "https://git.javalsai.tuxcord.net/rust/const-macros" keywords = ["compile", "const", "macro"] categories = ["development-tools::procedural-macro-helpers"] -[lib] -proc-macro = true - [dependencies] +const-macros-proc = { path = "./const-macros-proc" } [lints.clippy] cargo = { level = "warn", priority = -1 } diff --git a/assets/DATA1 b/assets/DATA1 new file mode 100644 index 0000000..9733554 --- /dev/null +++ b/assets/DATA1 @@ -0,0 +1 @@ +This is just a bunch of bytes that will hash to a known sequence and I can test for validity diff --git a/assets/DATA2 b/assets/DATA2 new file mode 100644 index 0000000..b528632 Binary files /dev/null and b/assets/DATA2 differ diff --git a/const-macros-proc/.gitignore b/const-macros-proc/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/const-macros-proc/.gitignore @@ -0,0 +1 @@ +/target diff --git a/const-macros-proc/Cargo.toml b/const-macros-proc/Cargo.toml new file mode 100644 index 0000000..0fca1d6 --- /dev/null +++ b/const-macros-proc/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "const-macros-proc" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +magic = "0.16" +sha2 = "0.10" diff --git a/const-macros-proc/src/lib.rs b/const-macros-proc/src/lib.rs new file mode 100644 index 0000000..e098b06 --- /dev/null +++ b/const-macros-proc/src/lib.rs @@ -0,0 +1,126 @@ +#![feature(proc_macro_value)] +use std::{fs::File, io::Read}; + +use proc_macro::{Delimiter, Group, Literal, Punct, TokenStream, TokenTree}; +use sha2::Digest; + +fn read_path_literal(input: TokenStream) -> String { + println!("{input:?}"); + let mut input_iter = input.into_iter(); + let path_token = input_iter.next().expect("expected a path"); + assert!(input_iter.next().is_none(), "only expected one argument"); + + let path_literal = match path_token { + TokenTree::Group(path_group) => { + let mut path_stream = path_group.stream().into_iter(); + let path_literal = path_stream.next().expect("expected a str literal"); + assert!(path_stream.next().is_none(), "expected a str literal"); + + let TokenTree::Literal(path_literal) = path_literal else { + panic!("expected a str literal"); + }; + + path_literal + } + TokenTree::Literal(path_literal) => path_literal, + _ => panic!("expected a str literal"), + }; + + // eprintln!("{path_literal:?}"); + path_literal.str_value().expect("expected a str literal") +} + +fn extend_grouped( + ts: &mut TokenStream, + delim: Delimiter, + items: impl Iterator, +) { + let mut comma_ts = TokenStream::new(); + comma_ts.extend(std::iter::once(TokenTree::Punct(Punct::new( + ',', + proc_macro::Spacing::Alone, + )))); + + let mut tuple_content = TokenStream::new(); + tuple_content.extend(items.flat_map(|token| [token, comma_ts.clone()])); + + ts.extend(std::iter::once(TokenTree::Group(Group::new( + delim, + tuple_content, + )))); +} + +fn extend_array(ts: &mut TokenStream, array: &[u8]) { + extend_grouped( + ts, + Delimiter::Bracket, + array.iter().map(|&b| { + let mut ts = TokenStream::new(); + ts.extend(std::iter::once(TokenTree::Literal(Literal::u8_unsuffixed( + b, + )))); + ts + }), + ); +} + +fn extend_asset_tuple(ts: &mut TokenStream, buf: &[u8], mime: &str, sha256sum: [u8; 32]) { + extend_grouped( + ts, + Delimiter::Parenthesis, + [ + { + let mut ts = TokenStream::new(); + ts.extend(std::iter::once(TokenTree::Punct(Punct::new( + '&', + proc_macro::Spacing::Joint, + )))); + extend_array(&mut ts, buf); + ts + }, + { + let mut ts = TokenStream::new(); + ts.extend(std::iter::once(Literal::string(mime))); + ts + }, + { + let mut ts = TokenStream::new(); + extend_array(&mut ts, &sha256sum); + ts + }, + ] + .into_iter(), + ); +} + +fn get_mime(buf: &[u8]) -> String { + let cookie = + magic::Cookie::open(magic::cookie::Flags::MIME).expect("failure opening mime cookie"); + let cookie = cookie + .load(&magic::cookie::DatabasePaths::default()) + .expect("failure loading magic db"); + + cookie.buffer(buf).expect("error getting file mime") +} + +fn get_sha256sum(buf: &[u8]) -> [u8; 32] { + sha2::Sha256::digest(buf) + .as_slice() + .try_into() + .expect("failed to sha256sum file") +} + +#[proc_macro] +#[allow(clippy::missing_panics_doc)] +pub fn include_asset(input: TokenStream) -> TokenStream { + let path = read_path_literal(input); + let mut buf = Vec::new(); + File::open(path) + .expect("couldn't open file") + .read_to_end(&mut buf) + .expect("error reading file"); + + let mut ts = TokenStream::new(); + extend_asset_tuple(&mut ts, &buf, &get_mime(&buf), get_sha256sum(&buf)); + ts +} diff --git a/src/lib.rs b/src/lib.rs index 64e5d9b..a118a48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,61 @@ -// #[must_use] -// pub const fn add(left: u64, right: u64) -> u64 { -// left + right -// } +// So hand working with [`TokenStream`] might be a bad idea BUT I partially made this crate to +// learn about [`proc_macro`] so I'm not gonna use [`quote`] or [`syn`] -// #[cfg(test)] -// mod tests { -// use super::*; +pub mod file { + pub type IncludeAssetMacroTy = (&'static [u8], &'static str, [u8; 32]); -// #[test] -// fn it_works() { -// let result = add(2, 2); -// assert_eq!(result, 4); -// } -// } + pub struct FileAsset { + pub bytes: &'static [u8], + pub mime: &'static str, + pub shasum: [u8; 32], + } + + impl FileAsset { + #[must_use] + pub const fn from_tuple(tuple: IncludeAssetMacroTy) -> Self { + Self { + bytes: tuple.0, + mime: tuple.1, + shasum: tuple.2, + } + } + } + + impl From for FileAsset { + fn from(value: (&'static [u8], &'static str, [u8; 32])) -> Self { + Self::from_tuple(value) + } + } + + #[macro_export] + macro_rules! include_asset { + ($path:literal) => { + $crate::file::FileAsset::from_tuple($crate::file::_include_asset!($path)) + }; + } + pub use include_asset; + + pub use const_macros_proc::include_asset as _include_asset; + + #[cfg(test)] + pub mod test { + #[test] + fn works() { + const ASSET1: super::FileAsset = super::include_asset!("assets/DATA1"); + const ASSET2: super::FileAsset = super::include_asset!("assets/DATA2"); + + assert_eq!(ASSET1.bytes, b"This is just a bunch of bytes that will hash to a known sequence and I can test for validity\n"); + assert_eq!(ASSET1.mime, "text/plain; charset=us-ascii"); + assert_eq!( + ASSET1.shasum, + [ + 0x4f, 0x72, 0xae, 0x5a, 0x7e, 0x2d, 0x70, 0xfe, 0xc4, 0x13, 0xaf, 0x0e, 0x29, + 0x8f, 0x1b, 0x7a, 0x7c, 0x1d, 0x8d, 0x9c, 0xaf, 0x4f, 0xd6, 0x24, 0x46, 0x0d, + 0x2f, 0xe2, 0xd8, 0x1b, 0x83, 0x28, + ] + ); + + assert_eq!(ASSET2.mime, "image/png; charset=binary"); + } + } +}