diff --git a/rust/src/lorem.rs b/rust/src/lorem.rs new file mode 100644 index 000000000..b897873f5 --- /dev/null +++ b/rust/src/lorem.rs @@ -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::(), 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); + } +}