use std::sync::Arc;
use arrow::array::{ArrayRef, StringBuilder};
#[cfg(test)]
use arrow::array::{Array, AsArray};
use crate::context::Context;
use crate::rng::Rng;
const ALPHA_UPPER_NO_IOQ: &[u8] = b"ABCDEFGHJKLMNPRSTUVWXYZ";
#[cfg(test)]
const ALPHA_UPPER: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
pub(crate) fn build_string_array<F>(n: usize, avg_width: usize, mut build_row: F) -> ArrayRef
where
F: FnMut(&mut Vec<u8>),
{
let mut builder = StringBuilder::with_capacity(n, n * avg_width);
let mut buf = Vec::with_capacity(avg_width);
for _ in 0..n {
buf.clear();
build_row(&mut buf);
debug_assert!(
std::str::from_utf8(&buf).is_ok(),
"build_string_array: build_row produced invalid UTF-8 — see the safety invariant \
on this function's doc comment"
);
let s = unsafe { std::str::from_utf8_unchecked(&buf) };
builder.append_value(s);
}
Arc::new(builder.finish())
}
fn rand_char(chars: &[u8], rng: &mut Rng) -> u8 {
chars[rng.next_usize(chars.len())]
}
pub fn buf_digits(nums: &[u64], width: usize, watermark_mask: Option<u64>) -> ArrayRef {
let n = nums.len();
let mut builder = StringBuilder::with_capacity(n, n * width);
#[allow(clippy::uninit_vec)]
let mut s: Vec<u8> = {
let mut v = Vec::with_capacity(width);
unsafe { v.set_len(width) };
v
};
for num in nums {
let mut val = *num;
for j in (0..width).rev() {
s[j] = b'0' + (val % 10) as u8;
val /= 10;
}
if let Some(wm) = watermark_mask
&& width >= 3
{
let start = width - 3;
s[start] = b'0' + ((wm / 100) % 10) as u8;
s[start + 1] = b'0' + ((wm / 10) % 10) as u8;
s[start + 2] = b'0' + (wm % 10) as u8;
}
debug_assert!(
std::str::from_utf8(&s).is_ok(),
"buf_digits: non-digit byte in s"
);
builder.append_value(unsafe { std::str::from_utf8_unchecked(&s) });
}
Arc::new(builder.finish())
}
pub fn bytes_strings(chars: &[u8], n: usize, length: usize, rng: &mut Rng) -> ArrayRef {
let mut builder = StringBuilder::with_capacity(n, n * length);
#[allow(clippy::uninit_vec)]
let mut s: Vec<u8> = {
let mut v = Vec::with_capacity(length);
unsafe { v.set_len(length) };
v
};
for _ in 0..n {
for b in s.iter_mut() {
*b = rand_char(chars, rng);
}
debug_assert!(
std::str::from_utf8(&s).is_ok(),
"bytes_strings: non-ASCII byte in chars"
);
builder.append_value(unsafe { std::str::from_utf8_unchecked(&s) });
}
Arc::new(builder.finish())
}
fn watermark_buf(buf: &mut [u8], n_digits: usize, value: u64) {
let start = buf.len() - n_digits;
let mut v = value;
for j in (0..n_digits).rev() {
buf[start + j] = b'0' + (v % 10) as u8;
v /= 10;
}
}
macro_rules! push_digit {
($buf:ident, $val:expr) => {
$buf.push(b'0' + ($val as u8));
};
}
pub fn buf_phone(n: usize, rng: &mut Rng, ctx: &Context) -> ArrayRef {
let wm = ctx.watermark_3digits(0x50484f4e);
build_string_array(n, 21, |buf| {
let a = rng.next_usize(800) + 200; let line = rng.next_usize(100); let x = rng.next_usize(9900) + 100; buf.push(b'+');
buf.push(b'1');
buf.push(b'-');
push_digit!(buf, a / 100);
push_digit!(buf, (a / 10) % 10);
push_digit!(buf, a % 10);
buf.push(b'-');
buf.extend_from_slice(b"555-01");
push_digit!(buf, line / 10);
push_digit!(buf, line % 10);
buf.push(b' ');
buf.push(b'x');
push_digit!(buf, x / 1000);
push_digit!(buf, (x / 100) % 10);
push_digit!(buf, (x / 10) % 10);
push_digit!(buf, x % 10);
watermark_buf(buf, 2, wm / 10);
})
}
pub fn buf_ssn(n: usize, rng: &mut Rng, ctx: &Context) -> ArrayRef {
let wm = ctx.watermark_3digits(0x53534e);
build_string_array(n, 11, |buf| {
let a = rng.next_usize(100) + 900; let b = rng.next_usize(90) + 10; let c = rng.next_usize(9000) + 1000; push_digit!(buf, a / 100);
push_digit!(buf, (a / 10) % 10);
push_digit!(buf, a % 10);
buf.push(b'-');
push_digit!(buf, b / 10);
push_digit!(buf, b % 10);
buf.push(b'-');
push_digit!(buf, c / 1000);
push_digit!(buf, (c / 100) % 10);
push_digit!(buf, (c / 10) % 10);
push_digit!(buf, c % 10);
watermark_buf(buf, 1, wm % 10);
})
}
fn ascii_local_into(s: &str, buf: &mut Vec<u8>) {
for c in s.chars() {
let lc = c.to_ascii_lowercase();
if lc.is_ascii_alphanumeric() {
buf.push(lc as u8);
}
}
}
pub fn buf_email(n: usize, rng: &mut Rng, ctx: &Context) -> ArrayRef {
let first = ctx
.pool_store
.get("first_name")
.expect("pool 'first_name' not found");
let last = ctx
.pool_store
.get("last_name")
.expect("pool 'last_name' not found");
let mut f_buf: Vec<u8> = Vec::with_capacity(16);
let mut l_buf: Vec<u8> = Vec::with_capacity(16);
build_string_array(n, 38, |buf| {
f_buf.clear();
ascii_local_into(&first[rng.next_usize(first.len())], &mut f_buf);
if f_buf.is_empty() {
f_buf.extend_from_slice(b"user");
}
l_buf.clear();
ascii_local_into(&last[rng.next_usize(last.len())], &mut l_buf);
if l_buf.is_empty() {
l_buf.push(b'x');
}
match rng.next_usize(3) {
0 => {
buf.extend_from_slice(&f_buf);
buf.push(b'.');
buf.extend_from_slice(&l_buf);
}
1 => {
buf.push(f_buf[0]);
buf.extend_from_slice(&l_buf);
}
_ => {
buf.extend_from_slice(&f_buf);
}
}
let num = rng.next_usize(900_000) + 100_000; push_digit!(buf, num / 100_000);
push_digit!(buf, (num / 10_000) % 10);
push_digit!(buf, (num / 1_000) % 10);
push_digit!(buf, (num / 100) % 10);
push_digit!(buf, (num / 10) % 10);
push_digit!(buf, num % 10);
buf.extend_from_slice(b"@example.com");
})
}
pub fn buf_pan(n: usize, rng: &mut Rng, ctx: &Context) -> ArrayRef {
let wm = ctx.watermark_3digits(0x50414e);
build_string_array(n, 13, |buf| {
let a = rng.next_usize(900) + 100; let b = rng.next_usize(90) + 10; let c = rng.next_usize(900000) + 100000; push_digit!(buf, a / 100);
push_digit!(buf, (a / 10) % 10);
push_digit!(buf, a % 10);
buf.push(b'-');
push_digit!(buf, b / 10);
push_digit!(buf, b % 10);
buf.push(b'-');
push_digit!(buf, c / 100000);
push_digit!(buf, (c / 10000) % 10);
push_digit!(buf, (c / 1000) % 10);
push_digit!(buf, (c / 100) % 10);
push_digit!(buf, (c / 10) % 10);
push_digit!(buf, c % 10);
watermark_buf(buf, 3, wm);
})
}
pub fn buf_medicare(n: usize, rng: &mut Rng, ctx: &Context) -> ArrayRef {
let wm = ctx.watermark_3digits(0x4d4544);
build_string_array(n, 12, |buf| {
let a = rng.next_usize(9000) + 1000; let b = rng.next_usize(900) + 100; let c = rng.next_usize(900) + 100; push_digit!(buf, a / 1000);
push_digit!(buf, (a / 100) % 10);
push_digit!(buf, (a / 10) % 10);
push_digit!(buf, a % 10);
buf.push(b'-');
push_digit!(buf, b / 100);
push_digit!(buf, (b / 10) % 10);
push_digit!(buf, b % 10);
buf.push(b'-');
push_digit!(buf, c / 100);
push_digit!(buf, (c / 10) % 10);
push_digit!(buf, c % 10);
watermark_buf(buf, 2, wm / 10);
})
}
pub fn buf_office_phone(n: usize, rng: &mut Rng, ctx: &Context) -> ArrayRef {
let wm = ctx.watermark_3digits(0x4f4643);
build_string_array(n, 21, |buf| {
let a = rng.next_usize(800) + 200; let line = rng.next_usize(100); let x = rng.next_usize(9900) + 100; buf.push(b'+');
buf.push(b'1');
buf.push(b'-');
push_digit!(buf, a / 100);
push_digit!(buf, (a / 10) % 10);
push_digit!(buf, a % 10);
buf.push(b'-');
buf.extend_from_slice(b"555-01");
push_digit!(buf, line / 10);
push_digit!(buf, line % 10);
buf.push(b' ');
buf.push(b'x');
push_digit!(buf, x / 1000);
push_digit!(buf, (x / 100) % 10);
push_digit!(buf, (x / 10) % 10);
push_digit!(buf, x % 10);
watermark_buf(buf, 3, wm);
})
}
pub fn buf_passport(n: usize, rng: &mut Rng, ctx: &Context) -> ArrayRef {
let wm = ctx.watermark_3digits(0x504153);
build_string_array(n, 8, |buf| {
let letter = rand_char(ALPHA_UPPER_NO_IOQ, rng);
let nums = rng.next_usize(9000000) + 1000000;
buf.push(letter);
push_digit!(buf, nums / 1000000);
push_digit!(buf, (nums / 100000) % 10);
push_digit!(buf, (nums / 10000) % 10);
push_digit!(buf, (nums / 1000) % 10);
push_digit!(buf, (nums / 100) % 10);
push_digit!(buf, (nums / 10) % 10);
push_digit!(buf, nums % 10);
watermark_buf(buf, 3, wm);
})
}
pub fn buf_ssn_last4(n: usize, rng: &mut Rng) -> ArrayRef {
build_string_array(n, 11, |buf| {
let nums = rng.next_usize(9000) + 1000;
buf.extend_from_slice(b"XXX-XX-");
push_digit!(buf, nums / 1000);
push_digit!(buf, (nums / 100) % 10);
push_digit!(buf, (nums / 10) % 10);
push_digit!(buf, nums % 10);
})
}
pub fn buf_acct_num(n: usize, rng: &mut Rng, ctx: &Context) -> ArrayRef {
let wm = ctx.watermark_2digits(0x414354);
build_string_array(n, 11, |buf| {
let nums = rng.next_usize(9_000_000) + 1_000_000;
buf.extend_from_slice(b"****");
push_digit!(buf, nums / 1_000_000);
push_digit!(buf, (nums / 100_000) % 10);
push_digit!(buf, (nums / 10_000) % 10);
push_digit!(buf, (nums / 1_000) % 10);
push_digit!(buf, (nums / 100) % 10);
push_digit!(buf, (nums / 10) % 10);
push_digit!(buf, nums % 10);
watermark_buf(buf, 2, wm);
})
}
pub fn buf_branch(n: usize, rng: &mut Rng) -> ArrayRef {
build_string_array(n, 4, |buf| {
let letter = rand_char(ALPHA_UPPER_NO_IOQ, rng);
let nums = rng.next_usize(900) + 100;
buf.push(letter);
push_digit!(buf, nums / 100);
push_digit!(buf, (nums / 10) % 10);
push_digit!(buf, nums % 10);
})
}
pub fn buf_seat_number(n: usize, rng: &mut Rng) -> ArrayRef {
const SEAT_LETTERS: &[u8] = b"ABCDEF";
build_string_array(n, 3, |buf| {
let row = rng.next_usize(60) + 1;
if row >= 10 {
push_digit!(buf, row / 10);
}
push_digit!(buf, row % 10);
buf.push(rand_char(SEAT_LETTERS, rng));
})
}
pub fn buf_booking_reference(n: usize, rng: &mut Rng) -> ArrayRef {
const PNR_CHARS: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
build_string_array(n, 6, |buf| {
for _ in 0..6 {
buf.push(rand_char(PNR_CHARS, rng));
}
})
}
pub fn buf_frequent_flyer(n: usize, rng: &mut Rng) -> ArrayRef {
build_string_array(n, 11, |buf| {
buf.push(rand_char(ALPHA_UPPER_NO_IOQ, rng));
buf.push(rand_char(ALPHA_UPPER_NO_IOQ, rng));
let nums = rng.next_usize(900_000_000) + 100_000_000;
push_digit!(buf, nums / 100_000_000);
push_digit!(buf, (nums / 10_000_000) % 10);
push_digit!(buf, (nums / 1_000_000) % 10);
push_digit!(buf, (nums / 100_000) % 10);
push_digit!(buf, (nums / 10_000) % 10);
push_digit!(buf, (nums / 1_000) % 10);
push_digit!(buf, (nums / 100) % 10);
push_digit!(buf, (nums / 10) % 10);
push_digit!(buf, nums % 10);
})
}
#[cfg(test)]
mod tests {
use super::*;
fn test_rng() -> Rng {
Rng::new(42)
}
fn test_ctx_with_pools() -> Context {
let pools_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/pools");
Context::new("kyc", "en", pools_dir.to_str().unwrap()).unwrap()
}
#[test]
fn test_buf_phone() {
let mut rng = test_rng();
let ctx = Context::test();
let arr = buf_phone(5, &mut rng, &ctx);
assert_eq!(arr.len(), 5);
let s = arr.as_string::<i32>();
for i in 0..5 {
let v = s.value(i);
assert_eq!(v.len(), 21, "phone[{i}] = {v:?}");
assert!(v.starts_with("+1-"), "phone[{i}] = {v:?}");
assert!(
v.contains("-555-01"),
"phone[{i}] = {v:?} not in NANP fictional block"
);
}
}
#[test]
fn test_buf_ssn() {
let mut rng = test_rng();
let ctx = Context::test();
let arr = buf_ssn(5, &mut rng, &ctx);
let s = arr.as_string::<i32>();
for i in 0..5 {
let v = s.value(i);
assert_eq!(v.len(), 11, "ssn[{i}] = {v:?}");
let area: u32 = v[..3].parse().unwrap();
assert!(
(900..=999).contains(&area),
"ssn[{i}] = {v:?} area not in never-assigned SSA block"
);
}
}
#[test]
fn test_buf_email() {
let mut rng = test_rng();
let ctx = test_ctx_with_pools();
let arr = buf_email(200, &mut rng, &ctx);
let s = arr.as_string::<i32>();
let mut seen = std::collections::HashSet::new();
for i in 0..200 {
let v = s.value(i);
assert!(v.ends_with("@example.com"), "email[{i}] = {v:?}");
let local = &v[..v.len() - "@example.com".len()];
assert!(!local.is_empty(), "email[{i}] = {v:?} has empty local-part");
assert!(
local
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'.'),
"email[{i}] = {v:?} has non ascii-alnum local-part"
);
seen.insert(v.to_string());
}
assert!(
seen.len() > 150,
"expected high cardinality over 200 rows, got {} distinct",
seen.len()
);
}
#[test]
fn test_buf_pan() {
let mut rng = test_rng();
let ctx = Context::test();
let arr = buf_pan(5, &mut rng, &ctx);
let s = arr.as_string::<i32>();
for i in 0..5 {
assert_eq!(s.value(i).len(), 13, "pan[{i}] = {:?}", s.value(i));
}
}
#[test]
fn test_buf_medicare() {
let mut rng = test_rng();
let ctx = Context::test();
let arr = buf_medicare(5, &mut rng, &ctx);
let s = arr.as_string::<i32>();
for i in 0..5 {
assert_eq!(s.value(i).len(), 12, "medicare[{i}] = {:?}", s.value(i));
}
}
#[test]
fn test_buf_office_phone() {
let mut rng = test_rng();
let ctx = Context::test();
let arr = buf_office_phone(5, &mut rng, &ctx);
let s = arr.as_string::<i32>();
for i in 0..5 {
let v = s.value(i);
assert_eq!(v.len(), 21, "office_phone[{i}] = {v:?}");
assert!(v.contains(" x"), "office_phone[{i}] = {v:?}");
assert!(
v.contains("-555-01"),
"office_phone[{i}] = {v:?} not in NANP fictional block"
);
}
}
#[test]
fn test_buf_passport() {
let mut rng = test_rng();
let ctx = Context::test();
let arr = buf_passport(5, &mut rng, &ctx);
let s = arr.as_string::<i32>();
for i in 0..5 {
let v = s.value(i);
assert_eq!(v.len(), 8, "passport[{i}] = {v:?}");
assert!(v.as_bytes()[0].is_ascii_uppercase());
for j in 1..8 {
assert!(v.as_bytes()[j].is_ascii_digit(), "passport[{i}] = {v:?}");
}
}
}
#[test]
fn test_buf_ssn_last4() {
let mut rng = test_rng();
let arr = buf_ssn_last4(5, &mut rng);
let s = arr.as_string::<i32>();
for i in 0..5 {
let v = s.value(i);
assert_eq!(v.len(), 11, "ssn_last4[{i}] = {v:?}");
assert!(v.starts_with("XXX-XX-"));
assert!(v.as_bytes()[3] == b'-' && v.as_bytes()[6] == b'-');
for j in 7..11 {
assert!(v.as_bytes()[j].is_ascii_digit(), "ssn_last4[{i}] = {v:?}");
}
}
}
#[test]
fn test_buf_acct_num() {
let mut rng = test_rng();
let ctx = Context::test();
let arr = buf_acct_num(5, &mut rng, &ctx);
let s = arr.as_string::<i32>();
for i in 0..5 {
let v = s.value(i);
assert_eq!(v.len(), 11, "acct_num[{i}] = {v:?}");
assert!(v.starts_with("****"));
assert!(v.as_bytes()[4].is_ascii_digit());
}
}
#[test]
fn test_buf_branch() {
let mut rng = test_rng();
let arr = buf_branch(5, &mut rng);
let s = arr.as_string::<i32>();
for i in 0..5 {
let v = s.value(i);
assert_eq!(v.len(), 4, "branch[{i}] = {v:?}");
assert!(v.as_bytes()[0].is_ascii_uppercase());
for j in 1..4 {
assert!(v.as_bytes()[j].is_ascii_digit(), "branch[{i}] = {v:?}");
}
}
}
#[test]
fn test_bytes_strings() {
let mut rng = test_rng();
let arr = bytes_strings(ALPHA_UPPER, 10, 5, &mut rng);
let s = arr.as_string::<i32>();
assert_eq!(s.len(), 10);
for i in 0..10 {
let v = s.value(i);
assert_eq!(v.len(), 5, "bytes_strings[{i}] = {v:?}");
for c in v.bytes() {
assert!(c.is_ascii_uppercase(), "bytes_strings[{i}] bad byte = {c}");
}
}
}
#[test]
fn test_buf_digits() {
let nums = vec![123, 45, 7890, 0, 9999];
let arr = buf_digits(&nums, 4, None);
let s = arr.as_string::<i32>();
assert_eq!(s.value(0), "0123");
assert_eq!(s.value(1), "0045");
assert_eq!(s.value(2), "7890");
assert_eq!(s.value(3), "0000");
assert_eq!(s.value(4), "9999");
}
#[test]
fn test_deterministic() {
let mut rng_a = Rng::new(42);
let mut rng_b = Rng::new(42);
let ctx = Context::test();
let a = buf_phone(100, &mut rng_a, &ctx);
let b = buf_phone(100, &mut rng_b, &ctx);
let a_arr = a.as_string::<i32>();
let b_arr = b.as_string::<i32>();
for i in 0..100 {
assert_eq!(a_arr.value(i), b_arr.value(i), "mismatch at {i}");
}
}
}