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:
2026-07-12 09:16:59 +02:00
parent 8d62f75f16
commit 57a2f3bef4
+558
View File
@@ -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);
}
}