use std::alloc::{alloc, dealloc, Layout};
use crate::api;
use crate::messages::Refusal;
const STATUS_OK: u32 = 0;
const STATUS_REFUSED: u32 = 1;
const STATUS_NOT_UTF8: u32 = 2;
fn layout(len: u32) -> Layout {
Layout::array::<u8>((len as usize).max(1)).expect("a byte array fits in memory")
}
#[no_mangle]
pub unsafe extern "C" fn frazaro_alloc(len: u32) -> *mut u8 {
unsafe { alloc(layout(len)) }
}
#[no_mangle]
pub unsafe extern "C" fn frazaro_free(ptr: *mut u8, len: u32) {
if !ptr.is_null() {
unsafe { dealloc(ptr, layout(len)) }
}
}
unsafe fn bytes<'a>(ptr: *const u8, len: u32) -> &'a [u8] {
if ptr.is_null() || len == 0 {
&[]
} else {
unsafe { std::slice::from_raw_parts(ptr, len as usize) }
}
}
fn record(status: u32, line: u32, id: &str, text: &[u8], out_len: *mut u32) -> *mut u8 {
let total = 16 + id.len() + text.len();
let mut buf: Vec<u8> = Vec::with_capacity(total);
buf.extend_from_slice(&status.to_le_bytes());
buf.extend_from_slice(&line.to_le_bytes());
buf.extend_from_slice(&(id.len() as u32).to_le_bytes());
buf.extend_from_slice(&(text.len() as u32).to_le_bytes());
buf.extend_from_slice(id.as_bytes());
buf.extend_from_slice(text);
unsafe {
let p = frazaro_alloc(total as u32);
std::ptr::copy_nonoverlapping(buf.as_ptr(), p, total);
if !out_len.is_null() {
*out_len = total as u32;
}
p
}
}
fn text<'a>(b: &'a [u8], what: &str, out_len: *mut u32) -> Result<&'a str, *mut u8> {
std::str::from_utf8(b).map_err(|_| {
record(
STATUS_NOT_UTF8,
0,
"",
format!("{what} is not UTF-8").as_bytes(),
out_len,
)
})
}
type Inputs<'a> = (&'a str, &'a str, Vec<&'a str>);
#[allow(clippy::too_many_arguments)]
unsafe fn inputs<'a>(
program: *const u8,
program_len: u32,
prelude: *const u8,
prelude_len: u32,
books: *const u8,
books_len: u32,
out_len: *mut u32,
) -> Result<Inputs<'a>, *mut u8> {
let (program, prelude, books) = unsafe {
(
bytes(program, program_len),
bytes(prelude, prelude_len),
bytes(books, books_len),
)
};
let program = text(program, "the program", out_len)?;
let prelude = text(prelude, "the prelude", out_len)?;
let books = text(books, "a phrasebook", out_len)?;
let vocabs: Vec<&str> = if books.is_empty() {
Vec::new()
} else {
books.split('\0').collect()
};
Ok((program, prelude, vocabs))
}
#[allow(clippy::too_many_arguments)]
unsafe fn translate(
to_vba: bool,
program: *const u8,
program_len: u32,
prelude: *const u8,
prelude_len: u32,
books: *const u8,
books_len: u32,
out_len: *mut u32,
) -> *mut u8 {
let (program, prelude, vocabs) = match unsafe {
inputs(
program,
program_len,
prelude,
prelude_len,
books,
books_len,
out_len,
)
} {
Ok(t) => t,
Err(p) => return p,
};
let out = if to_vba {
api::english_translate_text_to_vba(program, prelude, &vocabs)
} else {
api::english_translate_text_to_vla(program, prelude, &vocabs)
};
match out {
Ok(t) => record(STATUS_OK, 0, "", t.as_bytes(), out_len),
Err(e) => record(
STATUS_REFUSED,
e.line,
&e.refusal.id,
e.refusal.text.as_bytes(),
out_len,
),
}
}
#[no_mangle]
pub unsafe extern "C" fn frazaro_translate_vla(
program: *const u8,
program_len: u32,
prelude: *const u8,
prelude_len: u32,
books: *const u8,
books_len: u32,
out_len: *mut u32,
) -> *mut u8 {
unsafe {
translate(
false,
program,
program_len,
prelude,
prelude_len,
books,
books_len,
out_len,
)
}
}
#[no_mangle]
pub unsafe extern "C" fn frazaro_translate_vba(
program: *const u8,
program_len: u32,
prelude: *const u8,
prelude_len: u32,
books: *const u8,
books_len: u32,
out_len: *mut u32,
) -> *mut u8 {
unsafe {
translate(
true,
program,
program_len,
prelude,
prelude_len,
books,
books_len,
out_len,
)
}
}
#[no_mangle]
pub unsafe extern "C" fn frazaro_build_xlsx(
program: *const u8,
program_len: u32,
prelude: *const u8,
prelude_len: u32,
books: *const u8,
books_len: u32,
out_len: *mut u32,
) -> *mut u8 {
let (program, prelude, vocabs) = match unsafe {
inputs(
program,
program_len,
prelude,
prelude_len,
books,
books_len,
out_len,
)
} {
Ok(t) => t,
Err(p) => return p,
};
match api::english_build_xlsx(program, prelude, &vocabs) {
Ok(bytes) => {
let digest = crate::sha256::sha256_hex(&bytes);
record(STATUS_OK, 0, &digest, &bytes, out_len)
}
Err(e) => record(
STATUS_REFUSED,
e.line,
&e.refusal.id,
e.refusal.text.as_bytes(),
out_len,
),
}
}
#[no_mangle]
pub unsafe extern "C" fn frazaro_view(
program: *const u8,
program_len: u32,
prelude: *const u8,
prelude_len: u32,
books: *const u8,
books_len: u32,
sheet: *const u8,
sheet_len: u32,
window: *const u8,
window_len: u32,
out_len: *mut u32,
) -> *mut u8 {
let (program, prelude, vocabs) = match unsafe {
inputs(
program,
program_len,
prelude,
prelude_len,
books,
books_len,
out_len,
)
} {
Ok(t) => t,
Err(p) => return p,
};
let sheet = match text(unsafe { bytes(sheet, sheet_len) }, "the sheet", out_len) {
Ok(s) => s,
Err(p) => return p,
};
let window = match text(unsafe { bytes(window, window_len) }, "the window", out_len) {
Ok(w) => w,
Err(p) => return p,
};
let window = if window.is_empty() {
None
} else {
Some(window)
};
match api::english_view(program, prelude, &vocabs, sheet, window) {
Ok(lines) => record(STATUS_OK, 0, "", lines.as_bytes(), out_len),
Err(e) => record(
STATUS_REFUSED,
e.line,
&e.refusal.id,
e.refusal.text.as_bytes(),
out_len,
),
}
}
unsafe fn name_of<'a>(
ptr: *const u8,
len: u32,
default: &'a str,
what: &str,
out_len: *mut u32,
) -> Result<&'a str, *mut u8> {
let b = unsafe { bytes(ptr, len) };
if b.is_empty() {
return Ok(default);
}
text(b, what, out_len)
}
fn answer(result: Result<String, Refusal>, out_len: *mut u32) -> *mut u8 {
match result {
Ok(lines) => record(STATUS_OK, 0, "", lines.as_bytes(), out_len),
Err(e) => record(STATUS_REFUSED, 0, &e.id, e.text.as_bytes(), out_len),
}
}
#[no_mangle]
pub unsafe extern "C" fn frazaro_reflect(
book: *const u8,
book_len: u32,
name: *const u8,
name_len: u32,
out_len: *mut u32,
) -> *mut u8 {
let label = match unsafe { name_of(name, name_len, "the file", "the file's name", out_len) } {
Ok(s) => s,
Err(p) => return p,
};
let book = unsafe { bytes(book, book_len) };
answer(api::reflect_relations(book, label), out_len)
}
#[no_mangle]
pub unsafe extern "C" fn frazaro_audit(
book: *const u8,
book_len: u32,
name: *const u8,
name_len: u32,
out_len: *mut u32,
) -> *mut u8 {
let label = match unsafe { name_of(name, name_len, "the file", "the file's name", out_len) } {
Ok(s) => s,
Err(p) => return p,
};
let book = unsafe { bytes(book, book_len) };
answer(api::audit_findings(book, label), out_len)
}
#[allow(clippy::too_many_arguments)]
#[no_mangle]
pub unsafe extern "C" fn frazaro_diff(
old: *const u8,
old_len: u32,
old_name: *const u8,
old_name_len: u32,
new: *const u8,
new_len: u32,
new_name: *const u8,
new_name_len: u32,
out_len: *mut u32,
) -> *mut u8 {
let old_label = match unsafe {
name_of(
old_name,
old_name_len,
"the old file",
"the old file's name",
out_len,
)
} {
Ok(s) => s,
Err(p) => return p,
};
let new_label = match unsafe {
name_of(
new_name,
new_name_len,
"the new file",
"the new file's name",
out_len,
)
} {
Ok(s) => s,
Err(p) => return p,
};
let (old, new) = unsafe { (bytes(old, old_len), bytes(new, new_len)) };
answer(api::diff_relations(old, old_label, new, new_label), out_len)
}
#[no_mangle]
pub unsafe extern "C" fn frazaro_vocab_gate(
text_ptr: *const u8,
text_len: u32,
name: *const u8,
name_len: u32,
allow_raw: u32,
out_len: *mut u32,
) -> *mut u8 {
let (t, n) = unsafe { (bytes(text_ptr, text_len), bytes(name, name_len)) };
let t = match text(t, "the phrasebook", out_len) {
Ok(s) => s,
Err(p) => return p,
};
let n = match text(n, "the phrasebook's name", out_len) {
Ok(s) => s,
Err(p) => return p,
};
match api::vocab_gate(t, n, allow_raw != 0) {
Ok(()) => record(STATUS_OK, 0, "", b"", out_len),
Err(e) => record(STATUS_REFUSED, 0, &e.id, e.text.as_bytes(), out_len),
}
}
#[no_mangle]
pub unsafe extern "C" fn frazaro_version_text(out_len: *mut u32) -> *mut u8 {
record(STATUS_OK, 0, "", crate::VERSION.as_bytes(), out_len)
}
#[cfg(test)]
mod tests {
use super::*;
const PRELUDE: &str = include_str!("../../scripts/prelude.vla");
const ENGLISH: &str = include_str!("../../scripts/polyglotta/english.vla");
const FIXTURE: &str = include_str!("../../scripts/build/fixture.txt");
const GOLDEN: &[u8] = include_bytes!("../../scripts/build/fixture_golden.xlsx");
const FIXTURE_XLSX: &[u8] = include_bytes!("../../scripts/reflect/fixture.xlsx");
const CHANGED_XLSX: &[u8] = include_bytes!("../../scripts/reflect/changed.xlsx");
const FIXTURE_ODS: &[u8] = include_bytes!("../../scripts/reflect/opendocument.ods");
struct Raw {
status: u32,
line: u32,
id: String,
bytes: Vec<u8>,
}
struct Rec {
status: u32,
line: u32,
id: String,
text: String,
}
fn u32_at(b: &[u8], i: usize) -> u32 {
u32::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]])
}
unsafe fn put(s: &[u8]) -> (*mut u8, u32) {
let p = unsafe { frazaro_alloc(s.len() as u32) };
unsafe { std::ptr::copy_nonoverlapping(s.as_ptr(), p, s.len()) };
(p, s.len() as u32)
}
unsafe fn take_raw(ptr: *mut u8, len: u32) -> Raw {
let b = unsafe { std::slice::from_raw_parts(ptr, len as usize) }.to_vec();
unsafe { frazaro_free(ptr, len) };
let (id_len, text_len) = (u32_at(&b, 8) as usize, u32_at(&b, 12) as usize);
assert_eq!(
b.len(),
16 + id_len + text_len,
"the record's lengths add up"
);
Raw {
status: u32_at(&b, 0),
line: u32_at(&b, 4),
id: String::from_utf8(b[16..16 + id_len].to_vec()).unwrap(),
bytes: b[16 + id_len..].to_vec(),
}
}
unsafe fn take(ptr: *mut u8, len: u32) -> Rec {
let r = unsafe { take_raw(ptr, len) };
Rec {
status: r.status,
line: r.line,
id: r.id,
text: String::from_utf8(r.bytes).unwrap(),
}
}
fn build(program: &[u8], books: &[&str]) -> Raw {
unsafe {
let (pp, pl) = put(program);
let (qp, ql) = put(PRELUDE.as_bytes());
let joined = books.join("\0");
let (bp, bl) = put(joined.as_bytes());
let mut out_len = 0u32;
let ptr = frazaro_build_xlsx(pp, pl, qp, ql, bp, bl, &mut out_len);
frazaro_free(pp, pl);
frazaro_free(qp, ql);
frazaro_free(bp, bl);
take_raw(ptr, out_len)
}
}
fn translate(to_vba: bool, program: &[u8], books: &[&str]) -> Rec {
unsafe {
let (pp, pl) = put(program);
let (qp, ql) = put(PRELUDE.as_bytes());
let joined = books.join("\0");
let (bp, bl) = put(joined.as_bytes());
let mut out_len = 0u32;
let ptr = if to_vba {
frazaro_translate_vba(pp, pl, qp, ql, bp, bl, &mut out_len)
} else {
frazaro_translate_vla(pp, pl, qp, ql, bp, bl, &mut out_len)
};
frazaro_free(pp, pl);
frazaro_free(qp, ql);
frazaro_free(bp, bl);
take(ptr, out_len)
}
}
fn gate(text: &str, allow_raw: u32) -> Rec {
unsafe {
let (tp, tl) = put(text.as_bytes());
let (np, nl) = put(b"pasted.vla");
let mut out_len = 0u32;
let ptr = frazaro_vocab_gate(tp, tl, np, nl, allow_raw, &mut out_len);
frazaro_free(tp, tl);
frazaro_free(np, nl);
take(ptr, out_len)
}
}
#[test]
fn a_translation_comes_back_as_a_status_0_record() {
let r = translate(false, b"Set total to 5.", &[ENGLISH]);
assert_eq!((r.status, r.line, r.id.as_str()), (0, 0, ""));
assert!(r.text.contains("(set! total 5)"), "{}", r.text);
let r = translate(true, b"Log 1.", &[ENGLISH]);
assert_eq!(r.status, 0);
assert!(r.text.contains("Debug.Print 1"), "{}", r.text);
}
#[test]
fn a_refusal_comes_back_with_its_id_and_line() {
let r = translate(false, b"Log 1.\nSet total to $5.", &[ENGLISH]);
assert_eq!(
(r.status, r.line, r.id.as_str()),
(1, 2, "english-unknown-character")
);
assert!(r.text.contains("'$'"), "{}", r.text);
let v = translate(true, b"Log 1.\nSet total to $5.", &[ENGLISH]);
assert_eq!((v.status, v.line, v.id), (r.status, r.line, r.id));
}
#[test]
fn phrasebooks_are_nul_separated_and_named_in_order() {
let r = translate(false, b"Log 1.", &[ENGLISH, "hello"]);
assert_eq!(
(r.status, r.line, r.id.as_str()),
(1, 0, "english-vocab-expected-directive")
);
assert!(r.text.starts_with("vocab-2 line 0: "), "{}", r.text);
let r = translate(false, b"Set total to 5.", &[]);
assert_eq!(r.status, 0);
assert!(r.text.contains("(set! total 5)"), "{}", r.text);
}
#[test]
fn an_input_that_is_not_utf8_is_status_2() {
let r = translate(false, &[0xFF, 0xFE, 0x41], &[ENGLISH]);
assert_eq!((r.status, r.line), (2, 0));
assert_eq!(r.text, "the program is not UTF-8");
}
#[test]
fn the_gate_refuses_a_raw_form_without_consent_and_a_capability_always() {
let raw = "(english-vla \"char cell {r:cell}\" (raw \"Debug.Print 2\"))";
let r = gate(raw, 0);
assert_eq!(
(r.status, r.id.as_str()),
(1, "english-vocab-raw-consent-declined")
);
assert!(r.text.contains("'pasted.vla' was not loaded"), "{}", r.text);
let r = gate(raw, 1);
assert_eq!((r.status, r.text.as_str()), (0, ""));
let r = gate("(requires-capability \"network\")", 1);
assert_eq!(
(r.status, r.id.as_str()),
(1, "english-vocab-requires-capability-ungranted")
);
let r = gate("(english-vla \"wobble {x:expr}\" (debug-print {x}))", 0);
assert_eq!(r.status, 0);
}
#[test]
fn the_version_text_and_the_allocator_s_edges() {
unsafe {
let mut out_len = 0u32;
let ptr = frazaro_version_text(&mut out_len);
let r = take(ptr, out_len);
assert_eq!((r.status, r.text.as_str()), (0, crate::VERSION));
let p = frazaro_alloc(0);
assert!(!p.is_null());
frazaro_free(p, 0);
frazaro_free(std::ptr::null_mut(), 5);
}
}
#[test]
fn a_build_comes_back_as_the_golden_s_bytes_with_their_digest_as_its_id() {
let r = build(FIXTURE.as_bytes(), &[ENGLISH]);
assert_eq!((r.status, r.line), (0, 0));
assert_eq!(r.id, crate::sha256::sha256_hex(GOLDEN));
assert_eq!(r.bytes.len(), GOLDEN.len());
assert!(r.bytes == GOLDEN, "the record's bytes are the golden");
let r = build(FIXTURE.trim_end().as_bytes(), &[ENGLISH]);
assert!(
r.bytes == GOLDEN,
"a program without a final newline builds the same"
);
}
#[test]
fn a_build_s_refusals_come_back_through_the_record() {
let r = build(b"Put 5 into cell B2.\nSay \"hello\".\n", &[ENGLISH]);
assert_eq!(
(r.status, r.line, r.id.as_str()),
(1, 0, "build-not-representable")
);
let text = String::from_utf8(r.bytes).unwrap();
assert!(
text.contains("Line 2 asks for more: Say \"hello\"."),
"{text}"
);
let r = build(b"Put 5 into cell B2.\nSet total to $5.\n", &[ENGLISH]);
assert_eq!(
(r.status, r.line, r.id.as_str()),
(1, 2, "english-unknown-character")
);
let r = build(&[0xFF, 0xFE, 0x41], &[ENGLISH]);
assert_eq!((r.status, r.line), (2, 0));
assert_eq!(
String::from_utf8(r.bytes).unwrap(),
"the program is not UTF-8"
);
}
fn view(program: &[u8], books: &[&str], sheet: &[u8], window: &[u8]) -> Rec {
unsafe {
let (pp, pl) = put(program);
let (qp, ql) = put(PRELUDE.as_bytes());
let joined = books.join("\0");
let (bp, bl) = put(joined.as_bytes());
let (sp, sl) = put(sheet);
let (wp, wl) = put(window);
let mut out_len = 0u32;
let ptr = frazaro_view(pp, pl, qp, ql, bp, bl, sp, sl, wp, wl, &mut out_len);
frazaro_free(pp, pl);
frazaro_free(qp, ql);
frazaro_free(bp, bl);
frazaro_free(sp, sl);
frazaro_free(wp, wl);
take(ptr, out_len)
}
}
#[test]
fn a_view_comes_back_as_the_lines_the_door_prints() {
let r = view(FIXTURE.as_bytes(), &[ENGLISH], b"Output", b"A1:F20");
assert_eq!((r.status, r.line, r.id.as_str()), (0, 0, ""));
assert_eq!(
r.text,
golden(include_str!("../../scripts/view/fixture_output.vla"))
);
let r = view(FIXTURE.as_bytes(), &[ENGLISH], b"output", b"B2:C3");
assert_eq!(
r.text,
golden(include_str!("../../scripts/view/fixture_output_b2_c3.vla"))
);
let r = view(FIXTURE.as_bytes(), &[ENGLISH], b"Frazaro", b"");
assert_eq!(
r.text,
golden(include_str!("../../scripts/view/fixture_frazaro.vla"))
);
let r = view(FIXTURE.as_bytes(), &[ENGLISH], b"Summary", b"");
assert_eq!(
(r.status, r.line, r.id.as_str()),
(1, 0, "view-sheet-unknown")
);
assert!(r.text.contains("Frazaro, Output, data"), "{}", r.text);
let r = view(FIXTURE.as_bytes(), &[ENGLISH], b"Output", b"A:A");
assert_eq!(
(r.status, r.line, r.id.as_str()),
(1, 0, "view-window-not-a-range")
);
let r = view(
b"Put 5 into cell B2.\nSet total to $5.\n",
&[ENGLISH],
b"Output",
b"",
);
assert_eq!(
(r.status, r.line, r.id.as_str()),
(1, 2, "english-unknown-character")
);
let r = view(FIXTURE.as_bytes(), &[ENGLISH], &[0xFF], b"");
assert_eq!((r.status, r.text.as_str()), (2, "the sheet is not UTF-8"));
let r = view(FIXTURE.as_bytes(), &[ENGLISH], b"Output", &[0xFF]);
assert_eq!((r.status, r.text.as_str()), (2, "the window is not UTF-8"));
}
fn read_file(kind: &str, book: &[u8], name: &[u8]) -> Rec {
unsafe {
let (bp, bl) = put(book);
let (np, nl) = put(name);
let mut out_len = 0u32;
let ptr = if kind == "audit" {
frazaro_audit(bp, bl, np, nl, &mut out_len)
} else {
frazaro_reflect(bp, bl, np, nl, &mut out_len)
};
frazaro_free(bp, bl);
frazaro_free(np, nl);
take(ptr, out_len)
}
}
fn diff_files(old: &[u8], new: &[u8]) -> Rec {
unsafe {
let (op, ol) = put(old);
let (onp, onl) = put(b"old.xlsx");
let (np, nl) = put(new);
let (nnp, nnl) = put(b"new.xlsx");
let mut out_len = 0u32;
let ptr = frazaro_diff(op, ol, onp, onl, np, nl, nnp, nnl, &mut out_len);
frazaro_free(op, ol);
frazaro_free(onp, onl);
frazaro_free(np, nl);
frazaro_free(nnp, nnl);
take(ptr, out_len)
}
}
fn golden(text: &str) -> String {
text.replace("\r\n", "\n")
}
#[test]
fn the_reader_s_three_answer_with_the_lines_the_door_prints() {
let r = read_file("reflect", FIXTURE_XLSX, b"fixture.xlsx");
assert_eq!((r.status, r.line, r.id.as_str()), (0, 0, ""));
assert_eq!(
r.text,
golden(include_str!("../../scripts/reflect/fixture_relations.vla"))
);
let r = read_file("audit", FIXTURE_XLSX, b"fixture.xlsx");
assert_eq!(r.status, 0);
assert_eq!(
r.text,
golden(include_str!("../../scripts/reflect/fixture_audit.vla"))
);
let r = diff_files(FIXTURE_XLSX, CHANGED_XLSX);
assert_eq!(r.status, 0);
assert_eq!(
r.text,
golden(include_str!(
"../../scripts/reflect/fixture_changed_diff.vla"
))
);
let r = read_file("reflect", FIXTURE_ODS, b"opendocument.ods");
assert_eq!(r.status, 0);
assert_eq!(
r.text,
golden(include_str!(
"../../scripts/reflect/opendocument_relations.vla"
))
);
}
#[test]
fn a_file_that_is_no_workbook_is_refused_under_the_name_given() {
let r = read_file("reflect", b"(sheet \"Model\" visible)\n", b"relations.vla");
assert_eq!(
(r.status, r.line, r.id.as_str()),
(1, 0, "reflect-not-a-workbook")
);
assert!(
r.text.starts_with("relations.vla is not a workbook"),
"{}",
r.text
);
let r = read_file("audit", b"not a zip", b"");
assert_eq!(r.id, "reflect-not-a-workbook");
assert!(
r.text.starts_with("the file is not a workbook"),
"{}",
r.text
);
let r = diff_files(FIXTURE_XLSX, b"not a zip");
assert_eq!(r.status, 1);
assert!(
r.text.starts_with("new.xlsx is not a workbook"),
"{}",
r.text
);
let r = read_file("reflect", FIXTURE_XLSX, &[0xFF, 0xFE]);
assert_eq!(
(r.status, r.text.as_str()),
(2, "the file's name is not UTF-8")
);
}
#[test]
fn nothing_to_say_is_an_empty_status_0() {
let r = diff_files(FIXTURE_XLSX, FIXTURE_XLSX);
assert_eq!((r.status, r.id.as_str(), r.text.as_str()), (0, "", ""));
}
}