feat(cli): port the lorem generator to Rust
Move LOREM_genBlock and LOREM_genBuffer into the Rust CLI helper archive, preserving the frozen word distribution, seeded sentence and paragraph layout, tail filling, and C-compatible buffer ABI. Use per-call state so generation is reentrant without changing the generated bytes. Focused tests cover C reference vectors, distribution construction, determinism, zero-sized null buffers, tiny tail-filled buffers, and continued nonzero-fill generation. Test Plan: - cargo test --manifest-path rust/cli/Cargo.toml --no-default-features lorem::tests - cargo clippy --manifest-path rust/cli/Cargo.toml --all-targets -- -D warnings Refs: programs/lorem.c compatibility port
This commit is contained in:
@@ -0,0 +1,558 @@
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
//! Lorem ipsum generator for the command-line programs.
|
||||
//!
|
||||
//! Port of `programs/lorem.c`. Output is byte-identical to the C generator
|
||||
//! for a given `(size, seed, first, fill)` call, including the trailing
|
||||
//! sentence/paragraph punctuation and the buffer-end fill.
|
||||
//!
|
||||
//! The C unit keeps its cursor and random root in file-scope statics and is
|
||||
//! documented as sequential-only. The port carries the same values in a
|
||||
//! per-call state struct instead: the word-distribution table depends only
|
||||
//! on constants, so building it per call yields the identical table the C
|
||||
//! code caches in a global.
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::os::raw::{c_int, c_uint};
|
||||
|
||||
const WORDS: [&str; 255] = [
|
||||
"lorem",
|
||||
"ipsum",
|
||||
"dolor",
|
||||
"sit",
|
||||
"amet",
|
||||
"consectetur",
|
||||
"adipiscing",
|
||||
"elit",
|
||||
"sed",
|
||||
"do",
|
||||
"eiusmod",
|
||||
"tempor",
|
||||
"incididunt",
|
||||
"ut",
|
||||
"labore",
|
||||
"et",
|
||||
"dolore",
|
||||
"magna",
|
||||
"aliqua",
|
||||
"dis",
|
||||
"lectus",
|
||||
"vestibulum",
|
||||
"mattis",
|
||||
"ullamcorper",
|
||||
"velit",
|
||||
"commodo",
|
||||
"a",
|
||||
"lacus",
|
||||
"arcu",
|
||||
"magnis",
|
||||
"parturient",
|
||||
"montes",
|
||||
"nascetur",
|
||||
"ridiculus",
|
||||
"mus",
|
||||
"mauris",
|
||||
"nulla",
|
||||
"malesuada",
|
||||
"pellentesque",
|
||||
"eget",
|
||||
"gravida",
|
||||
"in",
|
||||
"dictum",
|
||||
"non",
|
||||
"erat",
|
||||
"nam",
|
||||
"voluptat",
|
||||
"maecenas",
|
||||
"blandit",
|
||||
"aliquam",
|
||||
"etiam",
|
||||
"enim",
|
||||
"lobortis",
|
||||
"scelerisque",
|
||||
"fermentum",
|
||||
"dui",
|
||||
"faucibus",
|
||||
"ornare",
|
||||
"at",
|
||||
"elementum",
|
||||
"eu",
|
||||
"facilisis",
|
||||
"odio",
|
||||
"morbi",
|
||||
"quis",
|
||||
"eros",
|
||||
"donec",
|
||||
"ac",
|
||||
"orci",
|
||||
"purus",
|
||||
"turpis",
|
||||
"cursus",
|
||||
"leo",
|
||||
"vel",
|
||||
"porta",
|
||||
"consequat",
|
||||
"interdum",
|
||||
"varius",
|
||||
"vulputate",
|
||||
"aliquet",
|
||||
"pharetra",
|
||||
"nunc",
|
||||
"auctor",
|
||||
"urna",
|
||||
"id",
|
||||
"metus",
|
||||
"viverra",
|
||||
"nibh",
|
||||
"cras",
|
||||
"mi",
|
||||
"unde",
|
||||
"omnis",
|
||||
"iste",
|
||||
"natus",
|
||||
"error",
|
||||
"perspiciatis",
|
||||
"voluptatem",
|
||||
"accusantium",
|
||||
"doloremque",
|
||||
"laudantium",
|
||||
"totam",
|
||||
"rem",
|
||||
"aperiam",
|
||||
"eaque",
|
||||
"ipsa",
|
||||
"quae",
|
||||
"ab",
|
||||
"illo",
|
||||
"inventore",
|
||||
"veritatis",
|
||||
"quasi",
|
||||
"architecto",
|
||||
"beatae",
|
||||
"vitae",
|
||||
"dicta",
|
||||
"sunt",
|
||||
"explicabo",
|
||||
"nemo",
|
||||
"ipsam",
|
||||
"quia",
|
||||
"voluptas",
|
||||
"aspernatur",
|
||||
"aut",
|
||||
"odit",
|
||||
"fugit",
|
||||
"consequuntur",
|
||||
"magni",
|
||||
"dolores",
|
||||
"eos",
|
||||
"qui",
|
||||
"ratione",
|
||||
"sequi",
|
||||
"nesciunt",
|
||||
"neque",
|
||||
"porro",
|
||||
"quisquam",
|
||||
"est",
|
||||
"dolorem",
|
||||
"adipisci",
|
||||
"numquam",
|
||||
"eius",
|
||||
"modi",
|
||||
"tempora",
|
||||
"incidunt",
|
||||
"magnam",
|
||||
"quaerat",
|
||||
"ad",
|
||||
"minima",
|
||||
"veniam",
|
||||
"nostrum",
|
||||
"ullam",
|
||||
"corporis",
|
||||
"suscipit",
|
||||
"laboriosam",
|
||||
"nisi",
|
||||
"aliquid",
|
||||
"ex",
|
||||
"ea",
|
||||
"commodi",
|
||||
"consequatur",
|
||||
"autem",
|
||||
"eum",
|
||||
"iure",
|
||||
"voluptate",
|
||||
"esse",
|
||||
"quam",
|
||||
"nihil",
|
||||
"molestiae",
|
||||
"illum",
|
||||
"fugiat",
|
||||
"quo",
|
||||
"pariatur",
|
||||
"vero",
|
||||
"accusamus",
|
||||
"iusto",
|
||||
"dignissimos",
|
||||
"ducimus",
|
||||
"blanditiis",
|
||||
"praesentium",
|
||||
"voluptatum",
|
||||
"deleniti",
|
||||
"atque",
|
||||
"corrupti",
|
||||
"quos",
|
||||
"quas",
|
||||
"molestias",
|
||||
"excepturi",
|
||||
"sint",
|
||||
"occaecati",
|
||||
"cupiditate",
|
||||
"provident",
|
||||
"similique",
|
||||
"culpa",
|
||||
"officia",
|
||||
"deserunt",
|
||||
"mollitia",
|
||||
"animi",
|
||||
"laborum",
|
||||
"dolorum",
|
||||
"fuga",
|
||||
"harum",
|
||||
"quidem",
|
||||
"rerum",
|
||||
"facilis",
|
||||
"expedita",
|
||||
"distinctio",
|
||||
"libero",
|
||||
"tempore",
|
||||
"cum",
|
||||
"soluta",
|
||||
"nobis",
|
||||
"eligendi",
|
||||
"optio",
|
||||
"cumque",
|
||||
"impedit",
|
||||
"minus",
|
||||
"quod",
|
||||
"maxime",
|
||||
"placeat",
|
||||
"facere",
|
||||
"possimus",
|
||||
"assumenda",
|
||||
"repellendus",
|
||||
"temporibus",
|
||||
"quibusdam",
|
||||
"officiis",
|
||||
"debitis",
|
||||
"saepe",
|
||||
"eveniet",
|
||||
"voluptates",
|
||||
"repudiandae",
|
||||
"recusandae",
|
||||
"itaque",
|
||||
"earum",
|
||||
"hic",
|
||||
"tenetur",
|
||||
"sapiente",
|
||||
"delectus",
|
||||
"reiciendis",
|
||||
"cillum",
|
||||
"maiores",
|
||||
"alias",
|
||||
"perferendis",
|
||||
"doloribus",
|
||||
"asperiores",
|
||||
"repellat",
|
||||
"minim",
|
||||
"nostrud",
|
||||
"exercitation",
|
||||
"ullamco",
|
||||
"laboris",
|
||||
"aliquip",
|
||||
"duis",
|
||||
"aute",
|
||||
"irure",
|
||||
];
|
||||
|
||||
/* simple 1-dimension distribution, based on word's length, favors small words */
|
||||
const WEIGHTS: [u32; 6] = [0, 8, 6, 4, 3, 2];
|
||||
|
||||
const DISTRIB_SIZE_MAX: usize = 650;
|
||||
|
||||
struct LoremState {
|
||||
ptr: *mut u8,
|
||||
nb_chars: usize,
|
||||
max_chars: usize,
|
||||
rand_root: u32,
|
||||
distrib: [u16; DISTRIB_SIZE_MAX],
|
||||
distrib_count: u32,
|
||||
}
|
||||
|
||||
impl LoremState {
|
||||
fn new(buffer: *mut u8, size: usize, seed: u32) -> Self {
|
||||
let mut state = LoremState {
|
||||
ptr: buffer,
|
||||
nb_chars: 0,
|
||||
max_chars: size,
|
||||
rand_root: seed,
|
||||
distrib: [0; DISTRIB_SIZE_MAX],
|
||||
distrib_count: 0,
|
||||
};
|
||||
let mut d = 0usize;
|
||||
for (w, word) in WORDS.iter().enumerate() {
|
||||
let len = word.len().min(WEIGHTS.len() - 1);
|
||||
for _ in 0..WEIGHTS[len] {
|
||||
state.distrib[d] = w as u16;
|
||||
d += 1;
|
||||
}
|
||||
}
|
||||
state.distrib_count = d as u32;
|
||||
debug_assert!(d <= DISTRIB_SIZE_MAX);
|
||||
state
|
||||
}
|
||||
|
||||
fn rand(&mut self, range: u32) -> u32 {
|
||||
const PRIME1: u32 = 2654435761;
|
||||
const PRIME2: u32 = 2246822519;
|
||||
let mut rand32 = self.rand_root;
|
||||
rand32 = rand32.wrapping_mul(PRIME1);
|
||||
rand32 ^= PRIME2;
|
||||
rand32 = rand32.rotate_left(13);
|
||||
self.rand_root = rand32;
|
||||
(((rand32 as u64) * (range as u64)) >> 32) as u32
|
||||
}
|
||||
|
||||
/* The C `about()` adds two stateful draws in one expression; this build's
|
||||
* compiler evaluates them left to right, which the reference vectors pin. */
|
||||
fn about(&mut self, target: u32) -> u32 {
|
||||
let a = self.rand(target);
|
||||
let b = self.rand(target);
|
||||
a + b + 1
|
||||
}
|
||||
|
||||
unsafe fn write_last_characters(&mut self) {
|
||||
let last_chars = self.max_chars - self.nb_chars;
|
||||
if last_chars == 0 {
|
||||
return;
|
||||
}
|
||||
unsafe { self.ptr.add(self.nb_chars).write(b'.') };
|
||||
self.nb_chars += 1;
|
||||
if last_chars > 2 {
|
||||
unsafe {
|
||||
std::ptr::write_bytes(self.ptr.add(self.nb_chars), b' ', last_chars - 2);
|
||||
}
|
||||
}
|
||||
if last_chars > 1 {
|
||||
unsafe { self.ptr.add(self.max_chars - 1).write(b'\n') };
|
||||
}
|
||||
self.nb_chars = self.max_chars;
|
||||
}
|
||||
|
||||
unsafe fn generate_word(&mut self, word: &str, separator: &str, up_case: bool) {
|
||||
let len = word.len() + separator.len();
|
||||
if self.nb_chars + len > self.max_chars {
|
||||
unsafe { self.write_last_characters() };
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(word.as_ptr(), self.ptr.add(self.nb_chars), word.len());
|
||||
}
|
||||
if up_case {
|
||||
let first = unsafe { self.ptr.add(self.nb_chars) };
|
||||
unsafe { first.write(first.read().wrapping_sub(b'a' - b'A')) };
|
||||
}
|
||||
self.nb_chars += word.len();
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
separator.as_ptr(),
|
||||
self.ptr.add(self.nb_chars),
|
||||
separator.len(),
|
||||
);
|
||||
}
|
||||
self.nb_chars += separator.len();
|
||||
}
|
||||
|
||||
unsafe fn generate_sentence(&mut self, nb_words: u32) {
|
||||
let comma_pos = self.about(9);
|
||||
let comma2 = comma_pos + self.about(7);
|
||||
let qmark = self.rand(11) == 7;
|
||||
let end_sep = if qmark { "? " } else { ". " };
|
||||
for i in 0..nb_words {
|
||||
let word_id = self.distrib[self.rand(self.distrib_count) as usize];
|
||||
let word = WORDS[word_id as usize];
|
||||
let mut sep = " ";
|
||||
if i == comma_pos {
|
||||
sep = ", ";
|
||||
}
|
||||
if i == comma2 {
|
||||
sep = ", ";
|
||||
}
|
||||
if i == nb_words - 1 {
|
||||
sep = end_sep;
|
||||
}
|
||||
unsafe { self.generate_word(word, sep, i == 0) };
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn generate_paragraph(&mut self, nb_sentences: u32) {
|
||||
for _ in 0..nb_sentences {
|
||||
let words_per_sentence = self.about(11);
|
||||
unsafe { self.generate_sentence(words_per_sentence) };
|
||||
}
|
||||
if self.nb_chars < self.max_chars {
|
||||
unsafe { self.ptr.add(self.nb_chars).write(b'\n') };
|
||||
self.nb_chars += 1;
|
||||
}
|
||||
if self.nb_chars < self.max_chars {
|
||||
unsafe { self.ptr.add(self.nb_chars).write(b'\n') };
|
||||
self.nb_chars += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* It's "common" for lorem ipsum generators to start with the same first
|
||||
* pre-defined sentence */
|
||||
unsafe fn generate_first_sentence(&mut self) {
|
||||
for (i, word) in WORDS[..18].iter().enumerate() {
|
||||
let separator = if i == 4 || i == 7 { ", " } else { " " };
|
||||
unsafe { self.generate_word(word, separator, i == 0) };
|
||||
}
|
||||
unsafe { self.generate_word(WORDS[18], ". ", false) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Rust implementation of the public `LOREM_genBlock()` ABI.
|
||||
///
|
||||
/// # Safety
|
||||
/// `buffer` must be valid for `size` writable bytes.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn LOREM_genBlock(
|
||||
buffer: *mut c_void,
|
||||
size: usize,
|
||||
seed: c_uint,
|
||||
first: c_int,
|
||||
fill: c_int,
|
||||
) -> usize {
|
||||
debug_assert!(size < i32::MAX as usize);
|
||||
let mut state = LoremState::new(buffer.cast::<u8>(), size, seed);
|
||||
|
||||
if first != 0 {
|
||||
unsafe { state.generate_first_sentence() };
|
||||
}
|
||||
while state.nb_chars < state.max_chars {
|
||||
let sentences_per_paragraph = state.about(7);
|
||||
unsafe { state.generate_paragraph(sentences_per_paragraph) };
|
||||
if fill == 0 {
|
||||
break; /* only generate one paragraph in not-fill mode */
|
||||
}
|
||||
}
|
||||
state.nb_chars
|
||||
}
|
||||
|
||||
/// Rust implementation of the public `LOREM_genBuffer()` ABI.
|
||||
///
|
||||
/// # Safety
|
||||
/// `buffer` must be valid for `size` writable bytes.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn LOREM_genBuffer(buffer: *mut c_void, size: usize, seed: c_uint) {
|
||||
unsafe { LOREM_genBlock(buffer, size, seed, 1, 1) };
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/* Reference vectors produced by the original C implementation. */
|
||||
|
||||
#[test]
|
||||
fn matches_c_reference_for_the_first_sentence() {
|
||||
let mut buf = [0u8; 96];
|
||||
let n = unsafe { LOREM_genBlock(buf.as_mut_ptr().cast(), buf.len(), 0, 1, 1) };
|
||||
assert_eq!(n, 96);
|
||||
assert_eq!(
|
||||
&buf[..],
|
||||
b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do \
|
||||
eiusmod tempor incididunt ut . \n"
|
||||
.as_slice()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_c_reference_for_a_seeded_paragraph() {
|
||||
let mut buf = [0u8; 96];
|
||||
let n = unsafe { LOREM_genBlock(buf.as_mut_ptr().cast(), buf.len(), 4242, 0, 0) };
|
||||
assert_eq!(n, 96);
|
||||
assert_eq!(
|
||||
&buf[..],
|
||||
b"Donec fuga veniam a temporibus tempor odio arcu delectus, velit. \
|
||||
Sit magnam elementum ipsum. . \n"
|
||||
.as_slice()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distribution_table_matches_the_c_totals() {
|
||||
let mut probe = [0u8; 1];
|
||||
/* Building the state exercises the distribution constructor. */
|
||||
let state = LoremState::new(probe.as_mut_ptr(), probe.len(), 0);
|
||||
assert_eq!(state.distrib_count, 641);
|
||||
assert!((state.distrib_count as usize) <= DISTRIB_SIZE_MAX);
|
||||
/* Every entry must reference a real word. */
|
||||
assert!(state.distrib[..state.distrib_count as usize]
|
||||
.iter()
|
||||
.all(|&w| (w as usize) < WORDS.len()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_sized_generation_accepts_a_null_buffer() {
|
||||
for first in [0, 1] {
|
||||
for fill in [0, 1] {
|
||||
let generated =
|
||||
unsafe { LOREM_genBlock(std::ptr::null_mut(), 0, 123, first, fill) };
|
||||
assert_eq!(generated, 0);
|
||||
}
|
||||
}
|
||||
unsafe { LOREM_genBuffer(std::ptr::null_mut(), 0, 123) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_first_sentence_buffers_use_the_c_tail_fill() {
|
||||
for size in 0..=5 {
|
||||
let mut buffer = [0xa5u8; 5];
|
||||
let generated = unsafe { LOREM_genBlock(buffer.as_mut_ptr().cast(), size, 0, 1, 1) };
|
||||
let expected = match size {
|
||||
0 => b"".as_slice(),
|
||||
1 => b".".as_slice(),
|
||||
2 => b".\n".as_slice(),
|
||||
3 => b". \n".as_slice(),
|
||||
4 => b". \n".as_slice(),
|
||||
5 => b". \n".as_slice(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert_eq!(generated, size);
|
||||
assert_eq!(&buffer[..size], expected);
|
||||
assert!(buffer[size..].iter().all(|&byte| byte == 0xa5));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonzero_fill_values_continue_until_the_buffer_is_full() {
|
||||
let mut one = vec![0xa5u8; 4096];
|
||||
let mut two = vec![0xa5u8; 4096];
|
||||
let one_size = unsafe { LOREM_genBlock(one.as_mut_ptr().cast(), one.len(), 7, 0, 1) };
|
||||
let two_size = unsafe { LOREM_genBlock(two.as_mut_ptr().cast(), two.len(), 7, 0, 2) };
|
||||
assert_eq!(one_size, one.len());
|
||||
assert_eq!(two_size, two.len());
|
||||
assert_eq!(one, two);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gen_buffer_is_deterministic_per_seed() {
|
||||
let mut a = vec![0u8; 4096];
|
||||
let mut b = vec![0u8; 4096];
|
||||
unsafe { LOREM_genBuffer(a.as_mut_ptr().cast(), a.len(), 99) };
|
||||
unsafe { LOREM_genBuffer(b.as_mut_ptr().cast(), b.len(), 99) };
|
||||
assert_eq!(a, b);
|
||||
unsafe { LOREM_genBuffer(b.as_mut_ptr().cast(), b.len(), 100) };
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user