#![doc = include_str!("../README.md")]
use proc_macro::{Delimiter, Group, Literal, Punct, Spacing, TokenStream, TokenTree};
use unsynn::{Comma, DelimitedVec, Parse, TokenIter};
const MAGIC: &[u8; 16] = b"STYX_SCHEMA_V2\0\0";
fn extract_schema_id(schema: &str) -> Result<String, String> {
let value = styx_tree::parse(schema).map_err(|e| format!("failed to parse schema: {e}"))?;
let obj = value
.as_object()
.ok_or_else(|| "schema root must be an object".to_string())?;
let meta = obj
.get("meta")
.ok_or_else(|| "schema must have a `meta` block".to_string())?;
let meta_obj = meta
.as_object()
.ok_or_else(|| "`meta` must be an object".to_string())?;
let id_value = meta_obj
.get("id")
.ok_or_else(|| "`meta` block must have an `id` field".to_string())?;
if let Some(s) = id_value.as_str() {
return Ok(s.to_string());
}
Err("`meta.id` must be a string or identifier".to_string())
}
fn sanitize_id(id: &str) -> String {
let mut result = String::with_capacity(id.len());
for c in id.chars() {
if c.is_ascii_alphanumeric() {
result.push(c);
} else {
result.push('_');
}
}
if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
result.insert(0, '_');
}
result
}
fn id_to_symbol_suffix(id: &str) -> String {
let sanitized = sanitize_id(id);
let hash = blake3::hash(id.as_bytes());
let bytes = hash.as_bytes();
format!(
"{}_{:02x}{:02x}{:02x}{:02x}",
sanitized, bytes[0], bytes[1], bytes[2], bytes[3]
)
}
fn build_embedded_blob(schema: &str) -> Vec<u8> {
let decompressed = schema.as_bytes();
let hash = blake3::hash(decompressed);
let compressed = lz4_flex::compress_prepend_size(decompressed);
let mut blob = Vec::with_capacity(16 + 4 + 4 + 32 + compressed.len());
blob.extend_from_slice(MAGIC);
blob.extend_from_slice(&(decompressed.len() as u32).to_le_bytes());
blob.extend_from_slice(&(compressed.len() as u32).to_le_bytes());
blob.extend_from_slice(hash.as_bytes());
blob.extend_from_slice(&compressed);
blob
}
fn parse_string_literal(lit: &unsynn::Literal) -> Option<String> {
let s = lit.to_string();
if let Some(after_r) = s.strip_prefix("r") {
let hash_count = after_r.chars().take_while(|&c| c == '#').count();
let prefix_len = hash_count + 1; let suffix_len = 1 + hash_count;
if after_r.len() >= prefix_len + suffix_len {
return Some(after_r[prefix_len..after_r.len() - suffix_len].to_string());
}
}
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
let inner = &s[1..s.len() - 1];
let mut result = String::new();
let mut chars = inner.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('r') => result.push('\r'),
Some('t') => result.push('\t'),
Some('\\') => result.push('\\'),
Some('"') => result.push('"'),
Some('0') => result.push('\0'),
Some(other) => {
result.push('\\');
result.push(other);
}
None => result.push('\\'),
}
} else {
result.push(c);
}
}
return Some(result);
}
None
}
fn generate_static(schema: &str) -> Result<TokenStream, String> {
let id = extract_schema_id(schema)?;
let suffix = id_to_symbol_suffix(&id);
let blob = build_embedded_blob(schema);
let blob_len = blob.len();
let mut array_contents = Vec::new();
for (i, byte) in blob.iter().enumerate() {
array_contents.push(TokenTree::Literal(Literal::u8_unsuffixed(*byte)));
if i < blob.len() - 1 {
array_contents.push(TokenTree::Punct(Punct::new(',', Spacing::Alone)));
}
}
let output = format!(
r#"
#[used]
#[unsafe(no_mangle)]
#[cfg_attr(target_os = "macos", unsafe(link_section = "__DATA,__styx_schemas"))]
#[cfg_attr(target_os = "linux", unsafe(link_section = ".styx_schemas"))]
#[cfg_attr(target_os = "windows", unsafe(link_section = ".styx"))]
static __STYX_SCHEMA_{suffix}: [u8; {blob_len}] = "#
);
let mut result: TokenStream = output.parse().unwrap();
let array_group = TokenTree::Group(Group::new(
Delimiter::Bracket,
array_contents.into_iter().collect(),
));
result.extend(std::iter::once(array_group));
result.extend(";".parse::<TokenStream>().unwrap());
Ok(result)
}
#[proc_macro]
pub fn embed_inline(input: TokenStream) -> TokenStream {
let mut tokens = TokenIter::new(proc_macro2::TokenStream::from(input));
let literal: unsynn::Literal = match Parse::parse(&mut tokens) {
Ok(l) => l,
Err(e) => {
return format!("compile_error!(\"expected string literal: {e}\")")
.parse()
.unwrap();
}
};
let schema = match parse_string_literal(&literal) {
Some(s) => s,
None => {
return "compile_error!(\"expected string literal\")"
.parse()
.unwrap();
}
};
match generate_static(&schema) {
Ok(ts) => ts,
Err(e) => format!("compile_error!(\"{}\")", e.replace('"', "\\\""))
.parse()
.unwrap(),
}
}
#[proc_macro]
pub fn embed_file(input: TokenStream) -> TokenStream {
let mut tokens = TokenIter::new(proc_macro2::TokenStream::from(input));
let literal: unsynn::Literal = match Parse::parse(&mut tokens) {
Ok(l) => l,
Err(e) => {
return format!("compile_error!(\"expected file path string: {e}\")")
.parse()
.unwrap();
}
};
let path = match parse_string_literal(&literal) {
Some(s) => s,
None => {
return "compile_error!(\"expected string literal for file path\")"
.parse()
.unwrap();
}
};
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
return format!("compile_error!(\"failed to read {}: {}\")", path, e)
.parse()
.unwrap();
}
};
match generate_static(&content) {
Ok(ts) => ts,
Err(e) => format!("compile_error!(\"{}\")", e.replace('"', "\\\""))
.parse()
.unwrap(),
}
}
#[proc_macro]
pub fn embed_files(input: TokenStream) -> TokenStream {
let mut tokens = TokenIter::new(proc_macro2::TokenStream::from(input));
let literals: DelimitedVec<unsynn::Literal, Comma> = match Parse::parse(&mut tokens) {
Ok(l) => l,
Err(e) => {
return format!("compile_error!(\"expected file path strings: {e}\")")
.parse()
.unwrap();
}
};
let mut result = TokenStream::new();
for delimited in literals.iter() {
let path = match parse_string_literal(&delimited.value) {
Some(s) => s,
None => {
return "compile_error!(\"expected string literal for file path\")"
.parse()
.unwrap();
}
};
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
return format!("compile_error!(\"failed to read {}: {}\")", path, e)
.parse()
.unwrap();
}
};
match generate_static(&content) {
Ok(ts) => result.extend(ts),
Err(e) => {
return format!("compile_error!(\"{}\")", e.replace('"', "\\\""))
.parse()
.unwrap();
}
}
}
if result.is_empty() {
return "compile_error!(\"embed_files! requires at least one file\")"
.parse()
.unwrap();
}
result
}
#[proc_macro]
pub fn embed_outdir_file(input: TokenStream) -> TokenStream {
let mut tokens = TokenIter::new(proc_macro2::TokenStream::from(input));
let literal: unsynn::Literal = match Parse::parse(&mut tokens) {
Ok(l) => l,
Err(e) => {
return format!("compile_error!(\"expected filename string: {e}\")")
.parse()
.unwrap();
}
};
let filename = match parse_string_literal(&literal) {
Some(s) => s,
None => {
return "compile_error!(\"expected string literal for filename\")"
.parse()
.unwrap();
}
};
let out_dir = match std::env::var("OUT_DIR") {
Ok(dir) => dir,
Err(_) => {
return "compile_error!(\"OUT_DIR not set - this macro must be used in a crate with a build script\")"
.parse()
.unwrap()
}
};
let path = std::path::Path::new(&out_dir).join(&filename);
let path_str = path.display().to_string();
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
return format!("compile_error!(\"failed to read {}: {}\")", path_str, e)
.parse()
.unwrap();
}
};
match generate_static(&content) {
Ok(ts) => ts,
Err(e) => format!("compile_error!(\"{}\")", e.replace('"', "\\\""))
.parse()
.unwrap(),
}
}
#[proc_macro]
pub fn embed_schema(input: TokenStream) -> TokenStream {
embed_inline(input)
}
#[proc_macro]
pub fn embed_schemas(input: TokenStream) -> TokenStream {
embed_inline(input)
}