sql_dialect_fmt_wasm/lib.rs
1//! Raw WebAssembly bindings for browser extension use.
2//!
3//! This crate deliberately avoids `wasm-bindgen` so the Chrome extension can load a single local
4//! `.wasm` file without generated JavaScript glue. JavaScript owns input buffers allocated through
5//! `sql_dialect_fmt_alloc`; this module owns the last formatted result until the next call or
6//! `sql_dialect_fmt_clear_result`.
7
8use std::{mem, ptr, slice, str};
9
10use sql_dialect_fmt_formatter::{format, FormatOptions};
11
12static mut LAST_RESULT_PTR: *mut u8 = ptr::null_mut();
13static mut LAST_RESULT_LEN: u32 = 0;
14
15/// Allocate a writable byte buffer in Wasm memory.
16///
17/// JavaScript should copy UTF-8 bytes into the returned pointer, call `sql_dialect_fmt_format`, then free
18/// this input buffer with `sql_dialect_fmt_dealloc(ptr, len)`.
19#[no_mangle]
20pub extern "C" fn sql_dialect_fmt_alloc(len: u32) -> u32 {
21 let mut buffer = Vec::<u8>::with_capacity(len as usize);
22 let ptr = buffer.as_mut_ptr();
23 mem::forget(buffer);
24 ptr as u32
25}
26
27/// Free a buffer previously allocated by `sql_dialect_fmt_alloc`.
28///
29/// # Safety
30///
31/// `ptr` must be a pointer returned by `sql_dialect_fmt_alloc` with the same `capacity`, and it must not
32/// have already been freed. Passing any other pointer or capacity is undefined behavior.
33#[no_mangle]
34pub unsafe extern "C" fn sql_dialect_fmt_dealloc(ptr: u32, capacity: u32) {
35 if ptr == 0 || capacity == 0 {
36 return;
37 }
38 drop(Vec::from_raw_parts(ptr as *mut u8, 0, capacity as usize));
39}
40
41/// Format the UTF-8 SQL source stored at `ptr..ptr + len`.
42///
43/// Returns:
44/// - `0`: success; read `sql_dialect_fmt_result_ptr()` and `sql_dialect_fmt_result_len()`.
45/// - `1`: invalid UTF-8 input; no result is stored.
46///
47/// # Safety
48///
49/// `ptr` must point to `len` initialized bytes in Wasm memory for the duration of the call.
50#[no_mangle]
51pub unsafe extern "C" fn sql_dialect_fmt_format(
52 ptr: u32,
53 len: u32,
54 line_width: u32,
55 indent_width: u32,
56 uppercase_keywords: u32,
57) -> u32 {
58 clear_last_result();
59
60 let bytes = slice::from_raw_parts(ptr as *const u8, len as usize);
61 let Ok(source) = str::from_utf8(bytes) else {
62 return 1;
63 };
64
65 let options = FormatOptions::default()
66 .with_line_width(line_width.max(1) as usize)
67 .with_indent_width(indent_width.clamp(1, 16) as usize)
68 .with_uppercase_keywords(uppercase_keywords != 0);
69
70 let mut result = format(source, &options).into_bytes().into_boxed_slice();
71 LAST_RESULT_LEN = result.len() as u32;
72 LAST_RESULT_PTR = result.as_mut_ptr();
73 mem::forget(result);
74 0
75}
76
77/// Pointer to the most recent formatted result.
78///
79/// # Safety
80///
81/// The returned pointer is valid only until the next `sql_dialect_fmt_format` or `sql_dialect_fmt_clear_result`
82/// call. Callers must pair it with `sql_dialect_fmt_result_len` and copy the bytes before releasing it.
83#[no_mangle]
84pub unsafe extern "C" fn sql_dialect_fmt_result_ptr() -> u32 {
85 LAST_RESULT_PTR as u32
86}
87
88/// Byte length of the most recent formatted result.
89///
90/// # Safety
91///
92/// The value describes the buffer returned by `sql_dialect_fmt_result_ptr` and is valid only until the next
93/// `sql_dialect_fmt_format` or `sql_dialect_fmt_clear_result` call.
94#[no_mangle]
95pub unsafe extern "C" fn sql_dialect_fmt_result_len() -> u32 {
96 LAST_RESULT_LEN
97}
98
99/// Release the most recent formatted result, if any.
100///
101/// # Safety
102///
103/// Callers must not read from a previously returned result pointer after this function runs.
104#[no_mangle]
105pub unsafe extern "C" fn sql_dialect_fmt_clear_result() {
106 clear_last_result();
107}
108
109unsafe fn clear_last_result() {
110 if !LAST_RESULT_PTR.is_null() {
111 let len = LAST_RESULT_LEN as usize;
112 if len > 0 {
113 drop(Box::from_raw(ptr::slice_from_raw_parts_mut(
114 LAST_RESULT_PTR,
115 len,
116 )));
117 }
118 LAST_RESULT_PTR = ptr::null_mut();
119 LAST_RESULT_LEN = 0;
120 }
121}