use std::{
alloc::{Layout, alloc, dealloc},
path::PathBuf,
};
use accent_sass_compiler::{InputSyntax, Options, OutputStyle, from_path, from_string};
pub const ACCENT_SASS_OK: u32 = 0;
pub const ACCENT_SASS_REJECTED: u32 = 1;
pub const ACCENT_SASS_NOT_UTF8: u32 = 2;
pub const ACCENT_SASS_STYLE_EXPANDED: u32 = 0;
pub const ACCENT_SASS_STYLE_COMPRESSED: u32 = 1;
pub const ACCENT_SASS_SYNTAX_SCSS: u32 = 0;
pub const ACCENT_SASS_SYNTAX_INDENTED: u32 = 1;
pub const ACCENT_SASS_SYNTAX_CSS: u32 = 2;
#[repr(C)]
#[derive(Debug)]
pub struct CompileResult {
pub status: u32,
pub ptr: *mut u8,
pub len: usize,
}
#[unsafe(no_mangle)]
pub extern "C" fn accent_sass_alloc(len: usize) -> *mut u8 {
if len == 0 {
return std::ptr::null_mut();
}
match Layout::from_size_align(len, 1) {
Ok(layout) => unsafe { alloc(layout) },
Err(_) => std::ptr::null_mut(),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn accent_sass_dealloc(ptr: *mut u8, len: usize) {
if ptr.is_null() || len == 0 {
return;
}
if let Ok(layout) = Layout::from_size_align(len, 1) {
unsafe { dealloc(ptr, layout) }
}
}
#[derive(Debug)]
pub struct CompileOptions {
style: OutputStyle,
syntax: Option<InputSyntax>,
load_paths: Vec<PathBuf>,
charset: bool,
alert_ascii: bool,
quiet: bool,
}
impl Default for CompileOptions {
fn default() -> Self {
Self {
style: OutputStyle::Expanded,
syntax: None,
load_paths: Vec::new(),
charset: true,
alert_ascii: false,
quiet: false,
}
}
}
impl CompileOptions {
fn as_options(&self) -> Options<'_> {
let options = Options::default()
.style(self.style)
.quiet(self.quiet)
.allows_charset(self.charset)
.unicode_error_messages(!self.alert_ascii)
.load_paths(&self.load_paths);
match self.syntax {
Some(syntax) => options.input_syntax(syntax),
None => options,
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn accent_sass_options_new() -> *mut CompileOptions {
Box::into_raw(Box::new(CompileOptions::default()))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn accent_sass_options_free(options: *mut CompileOptions) {
if options.is_null() {
return;
}
drop(unsafe { Box::from_raw(options) });
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn accent_sass_options_set_style(
options: *mut CompileOptions,
style: u32,
) -> u32 {
let style = match style {
ACCENT_SASS_STYLE_EXPANDED => OutputStyle::Expanded,
ACCENT_SASS_STYLE_COMPRESSED => OutputStyle::Compressed,
_ => return ACCENT_SASS_REJECTED,
};
unsafe { with(options, |options| options.style = style) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn accent_sass_options_set_syntax(
options: *mut CompileOptions,
syntax: u32,
) -> u32 {
let syntax = match syntax {
ACCENT_SASS_SYNTAX_SCSS => InputSyntax::Scss,
ACCENT_SASS_SYNTAX_INDENTED => InputSyntax::Sass,
ACCENT_SASS_SYNTAX_CSS => InputSyntax::Css,
_ => return ACCENT_SASS_REJECTED,
};
unsafe { with(options, |options| options.syntax = Some(syntax)) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn accent_sass_options_add_load_path(
options: *mut CompileOptions,
ptr: *const u8,
len: usize,
) -> u32 {
let path = match unsafe { borrow(ptr, len) } {
Ok(path) => PathBuf::from(path),
Err(_) => return ACCENT_SASS_NOT_UTF8,
};
unsafe { with(options, |options| options.load_paths.push(path)) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn accent_sass_options_set_charset(
options: *mut CompileOptions,
charset: u32,
) -> u32 {
unsafe { with(options, |options| options.charset = charset != 0) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn accent_sass_options_set_alert_ascii(
options: *mut CompileOptions,
alert_ascii: u32,
) -> u32 {
unsafe { with(options, |options| options.alert_ascii = alert_ascii != 0) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn accent_sass_options_set_quiet(
options: *mut CompileOptions,
quiet: u32,
) -> u32 {
unsafe { with(options, |options| options.quiet = quiet != 0) }
}
unsafe fn with<F: FnOnce(&mut CompileOptions)>(options: *mut CompileOptions, f: F) -> u32 {
if options.is_null() {
return ACCENT_SASS_REJECTED;
}
f(unsafe { &mut *options });
ACCENT_SASS_OK
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn accent_sass_compile_string(
ptr: *const u8,
len: usize,
) -> *mut CompileResult {
unsafe { compile_string(ptr, len, &CompileOptions::default()) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn accent_sass_compile_string_with_options(
ptr: *const u8,
len: usize,
options: *const CompileOptions,
) -> *mut CompileResult {
if options.is_null() {
return result(ACCENT_SASS_REJECTED, NULL_OPTIONS.to_owned());
}
unsafe { compile_string(ptr, len, &*options) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn accent_sass_compile_path(
ptr: *const u8,
len: usize,
) -> *mut CompileResult {
unsafe { compile_path(ptr, len, &CompileOptions::default()) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn accent_sass_compile_path_with_options(
ptr: *const u8,
len: usize,
options: *const CompileOptions,
) -> *mut CompileResult {
if options.is_null() {
return result(ACCENT_SASS_REJECTED, NULL_OPTIONS.to_owned());
}
unsafe { compile_path(ptr, len, &*options) }
}
const NULL_OPTIONS: &str = "Error: null options handle.";
unsafe fn compile_string(
ptr: *const u8,
len: usize,
options: &CompileOptions,
) -> *mut CompileResult {
match unsafe { borrow(ptr, len) } {
Ok(input) => match from_string(input.to_owned(), &options.as_options()) {
Ok(css) => result(ACCENT_SASS_OK, css),
Err(e) => result(ACCENT_SASS_REJECTED, e.to_string()),
},
Err(message) => result(ACCENT_SASS_NOT_UTF8, message),
}
}
unsafe fn compile_path(ptr: *const u8, len: usize, options: &CompileOptions) -> *mut CompileResult {
match unsafe { borrow(ptr, len) } {
Ok(path) => match from_path(path, &options.as_options()) {
Ok(css) => result(ACCENT_SASS_OK, css),
Err(e) => result(ACCENT_SASS_REJECTED, e.to_string()),
},
Err(message) => result(ACCENT_SASS_NOT_UTF8, message),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn accent_sass_result_free(res: *mut CompileResult) {
if res.is_null() {
return;
}
let res = unsafe { Box::from_raw(res) };
if !res.ptr.is_null() && res.len != 0 {
drop(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(res.ptr, res.len)) });
}
}
unsafe fn borrow<'a>(ptr: *const u8, len: usize) -> Result<&'a str, String> {
if ptr.is_null() || len == 0 {
return Ok("");
}
let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
std::str::from_utf8(bytes).map_err(|e| e.to_string())
}
fn result(status: u32, body: String) -> *mut CompileResult {
let bytes = body.into_bytes().into_boxed_slice();
let len = bytes.len();
let ptr = if len == 0 {
drop(bytes);
std::ptr::null_mut()
} else {
Box::into_raw(bytes).cast::<u8>()
};
Box::into_raw(Box::new(CompileResult { status, ptr, len }))
}
#[cfg(test)]
mod tests {
use super::*;
fn round_trip(
f: unsafe extern "C" fn(*const u8, usize) -> *mut CompileResult,
input: &str,
) -> (u32, String) {
let len = input.len();
let ptr = accent_sass_alloc(len);
unsafe { std::ptr::copy_nonoverlapping(input.as_ptr(), ptr, len) };
let res = unsafe { f(ptr, len) };
let (status, body) = unsafe {
let body = if (*res).len == 0 {
String::new()
} else {
String::from_utf8(std::slice::from_raw_parts((*res).ptr, (*res).len).to_vec())
.unwrap()
};
((*res).status, body)
};
unsafe {
accent_sass_result_free(res);
accent_sass_dealloc(ptr, len);
}
(status, body)
}
fn round_trip_with(
f: unsafe extern "C" fn(*const u8, usize, *const CompileOptions) -> *mut CompileResult,
input: &str,
options: *const CompileOptions,
) -> (u32, String) {
let len = input.len();
let ptr = accent_sass_alloc(len);
unsafe { std::ptr::copy_nonoverlapping(input.as_ptr(), ptr, len) };
let res = unsafe { f(ptr, len, options) };
let (status, body) = unsafe {
let body = if (*res).len == 0 {
String::new()
} else {
String::from_utf8(std::slice::from_raw_parts((*res).ptr, (*res).len).to_vec())
.unwrap()
};
((*res).status, body)
};
unsafe {
accent_sass_result_free(res);
accent_sass_dealloc(ptr, len);
}
(status, body)
}
fn add_load_path(options: *mut CompileOptions, path: &str) -> u32 {
let len = path.len();
let ptr = accent_sass_alloc(len);
unsafe { std::ptr::copy_nonoverlapping(path.as_ptr(), ptr, len) };
let status = unsafe { accent_sass_options_add_load_path(options, ptr, len) };
unsafe { accent_sass_dealloc(ptr, len) };
status
}
#[test]
fn compiles_a_string() {
let (status, body) = round_trip(accent_sass_compile_string, "a {\n b: 1px + 2px;\n}\n");
assert_eq!(status, 0);
assert_eq!(body, "a {\n b: 3px;\n}\n");
}
#[test]
fn reports_a_compile_error() {
let (status, body) = round_trip(accent_sass_compile_string, "a { b: 1px + ; }");
assert_eq!(status, 1);
assert!(body.starts_with("Error: "), "{body}");
}
#[test]
fn reports_a_missing_file() {
let (status, body) =
round_trip(accent_sass_compile_path, "there-is-no-such-stylesheet.scss");
assert_eq!(status, 1);
assert!(body.starts_with("Error: "), "{body}");
}
#[test]
fn rejects_invalid_utf8() {
let input = [0x61_u8, 0xff, 0x7b, 0x7d];
let ptr = accent_sass_alloc(input.len());
unsafe { std::ptr::copy_nonoverlapping(input.as_ptr(), ptr, input.len()) };
let res = unsafe { accent_sass_compile_string(ptr, input.len()) };
unsafe {
assert_eq!((*res).status, 2);
accent_sass_result_free(res);
accent_sass_dealloc(ptr, input.len());
}
}
#[test]
fn empty_input_compiles_to_nothing() {
let res = unsafe { accent_sass_compile_string(std::ptr::null(), 0) };
unsafe {
assert_eq!((*res).status, 0);
assert_eq!((*res).len, 0);
accent_sass_result_free(res);
}
}
#[test]
fn compresses_when_asked() {
let options = accent_sass_options_new();
unsafe {
assert_eq!(
accent_sass_options_set_style(options, ACCENT_SASS_STYLE_COMPRESSED),
ACCENT_SASS_OK
);
}
let (status, body) = round_trip_with(
accent_sass_compile_string_with_options,
"a {\n b: 1px + 2px;\n}\n",
options,
);
assert_eq!(status, ACCENT_SASS_OK);
assert_eq!(body, "a{b:3px}");
unsafe { accent_sass_options_free(options) };
}
#[test]
fn parses_the_indented_syntax_when_asked() {
let input = "a\n b: 1px + 2px\n";
let options = accent_sass_options_new();
let (status, _) = round_trip_with(accent_sass_compile_string_with_options, input, options);
assert_eq!(
status, ACCENT_SASS_REJECTED,
"indented input parsed as SCSS"
);
unsafe {
assert_eq!(
accent_sass_options_set_syntax(options, ACCENT_SASS_SYNTAX_INDENTED),
ACCENT_SASS_OK
);
}
let (status, body) =
round_trip_with(accent_sass_compile_string_with_options, input, options);
assert_eq!(status, ACCENT_SASS_OK);
assert_eq!(body, "a {\n b: 3px;\n}\n");
unsafe { accent_sass_options_free(options) };
}
#[test]
fn rejects_plain_css_input_when_the_syntax_says_css() {
let options = accent_sass_options_new();
unsafe {
assert_eq!(
accent_sass_options_set_syntax(options, ACCENT_SASS_SYNTAX_CSS),
ACCENT_SASS_OK
);
}
let (status, body) = round_trip_with(
accent_sass_compile_string_with_options,
"$a: 1px;\nb {\n c: $a;\n}\n",
options,
);
assert_eq!(status, ACCENT_SASS_REJECTED);
assert!(body.starts_with("Error: "), "{body}");
unsafe { accent_sass_options_free(options) };
}
#[test]
fn resolves_an_import_through_a_load_path() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("_shared.scss"), "$v: 2px;\n").unwrap();
let input = "@use \"shared\";\na {\n b: shared.$v + 1px;\n}\n";
let options = accent_sass_options_new();
let (status, _) = round_trip_with(accent_sass_compile_string_with_options, input, options);
assert_eq!(status, ACCENT_SASS_REJECTED);
assert_eq!(
add_load_path(options, dir.path().to_str().unwrap()),
ACCENT_SASS_OK
);
let (status, body) =
round_trip_with(accent_sass_compile_string_with_options, input, options);
assert_eq!(status, ACCENT_SASS_OK, "{body}");
assert_eq!(body, "a {\n b: 3px;\n}\n");
unsafe { accent_sass_options_free(options) };
}
#[test]
fn load_paths_apply_to_a_path_compile_too() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("_shared.scss"), "$v: 2px;\n").unwrap();
let entry = dir.path().join("entry.scss");
std::fs::write(&entry, "@use \"shared\";\na {\n b: shared.$v + 1px;\n}\n").unwrap();
let options = accent_sass_options_new();
assert_eq!(
add_load_path(options, dir.path().to_str().unwrap()),
ACCENT_SASS_OK
);
let (status, body) = round_trip_with(
accent_sass_compile_path_with_options,
entry.to_str().unwrap(),
options,
);
assert_eq!(status, ACCENT_SASS_OK, "{body}");
assert_eq!(body, "a {\n b: 3px;\n}\n");
unsafe { accent_sass_options_free(options) };
}
#[test]
fn charset_can_be_turned_off() {
let input = "a {\n b: \"\u{e9}\";\n}\n";
let options = accent_sass_options_new();
let (_, with_charset) =
round_trip_with(accent_sass_compile_string_with_options, input, options);
assert!(with_charset.starts_with("@charset"), "{with_charset}");
unsafe {
assert_eq!(accent_sass_options_set_charset(options, 0), ACCENT_SASS_OK);
}
let (_, without) = round_trip_with(accent_sass_compile_string_with_options, input, options);
assert!(!without.starts_with("@charset"), "{without}");
unsafe { accent_sass_options_free(options) };
}
#[test]
fn alert_ascii_keeps_error_messages_inside_ascii() {
let input = "a { b: 1px + ; }";
let options = accent_sass_options_new();
let (_, unicode) = round_trip_with(accent_sass_compile_string_with_options, input, options);
assert!(!unicode.is_ascii(), "{unicode}");
unsafe {
assert_eq!(
accent_sass_options_set_alert_ascii(options, 1),
ACCENT_SASS_OK
);
}
let (_, ascii) = round_trip_with(accent_sass_compile_string_with_options, input, options);
assert!(ascii.is_ascii(), "{ascii}");
unsafe { accent_sass_options_free(options) };
}
#[test]
fn an_undefined_value_is_refused_and_changes_nothing() {
let options = accent_sass_options_new();
unsafe {
assert_eq!(
accent_sass_options_set_style(options, ACCENT_SASS_STYLE_COMPRESSED),
ACCENT_SASS_OK
);
assert_eq!(
accent_sass_options_set_style(options, 7),
ACCENT_SASS_REJECTED
);
assert_eq!(
accent_sass_options_set_syntax(options, 7),
ACCENT_SASS_REJECTED
);
}
let (status, body) = round_trip_with(
accent_sass_compile_string_with_options,
"a {\n b: 1px + 2px;\n}\n",
options,
);
assert_eq!(status, ACCENT_SASS_OK);
assert_eq!(body, "a{b:3px}");
unsafe { accent_sass_options_free(options) };
}
#[test]
fn a_null_handle_is_refused_rather_than_ignored() {
unsafe {
assert_eq!(
accent_sass_options_set_style(std::ptr::null_mut(), ACCENT_SASS_STYLE_EXPANDED),
ACCENT_SASS_REJECTED
);
assert_eq!(
accent_sass_options_set_syntax(std::ptr::null_mut(), ACCENT_SASS_SYNTAX_SCSS),
ACCENT_SASS_REJECTED
);
assert_eq!(
accent_sass_options_set_charset(std::ptr::null_mut(), 1),
ACCENT_SASS_REJECTED
);
assert_eq!(
accent_sass_options_set_alert_ascii(std::ptr::null_mut(), 1),
ACCENT_SASS_REJECTED
);
assert_eq!(
accent_sass_options_set_quiet(std::ptr::null_mut(), 1),
ACCENT_SASS_REJECTED
);
assert_eq!(
add_load_path(std::ptr::null_mut(), "/tmp"),
ACCENT_SASS_REJECTED
);
accent_sass_options_free(std::ptr::null_mut());
}
let (status, body) = round_trip_with(
accent_sass_compile_string_with_options,
"a { b: 1px; }",
std::ptr::null(),
);
assert_eq!(status, ACCENT_SASS_REJECTED);
assert_eq!(body, NULL_OPTIONS);
}
#[test]
fn a_load_path_that_is_not_utf8_is_refused() {
let options = accent_sass_options_new();
let input = [0x2f_u8, 0xff];
let ptr = accent_sass_alloc(input.len());
unsafe { std::ptr::copy_nonoverlapping(input.as_ptr(), ptr, input.len()) };
unsafe {
assert_eq!(
accent_sass_options_add_load_path(options, ptr, input.len()),
ACCENT_SASS_NOT_UTF8
);
accent_sass_dealloc(ptr, input.len());
accent_sass_options_free(options);
}
}
#[test]
fn one_handle_drives_many_compiles() {
let options = accent_sass_options_new();
unsafe {
accent_sass_options_set_style(options, ACCENT_SASS_STYLE_COMPRESSED);
accent_sass_options_set_quiet(options, 1);
}
for _ in 0..3 {
let (status, body) = round_trip_with(
accent_sass_compile_string_with_options,
"@warn \"noisy\";\na {\n b: 1px + 2px;\n}\n",
options,
);
assert_eq!(status, ACCENT_SASS_OK);
assert_eq!(body, "a{b:3px}");
}
unsafe { accent_sass_options_free(options) };
}
#[test]
fn zero_sized_allocations_are_null_and_freeing_null_is_a_no_op() {
assert!(accent_sass_alloc(0).is_null());
unsafe {
accent_sass_dealloc(std::ptr::null_mut(), 0);
accent_sass_result_free(std::ptr::null_mut());
}
}
}