use std::fs::File;
use std::path::Path;
use std::sync::Arc;
use arrow::array::{Array, ArrayRef, RecordBatch, StringArray};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::ipc::reader::FileReader;
use arrow::ipc::writer::FileWriter;
use crate::secrets::MASK;
pub fn values(batch: &RecordBatch, columns: &[String]) -> Vec<String> {
let mut out = Vec::new();
for c in columns {
let Some(a) = batch.column_by_name(c) else {
continue;
};
let Ok(text) = arrow::compute::cast(a, &DataType::Utf8) else {
continue;
};
let Some(text) = text.as_any().downcast_ref::<StringArray>() else {
continue;
};
out.extend(text.iter().flatten().map(str::to_string));
}
out
}
pub fn register(spool: &Path, columns: &[String]) -> Result<(), String> {
if columns.is_empty() {
return Ok(());
}
let reader = FileReader::try_new(File::open(spool).map_err(|e| e.to_string())?, None)
.map_err(|e| e.to_string())?;
for b in reader {
crate::secrets::register_values(values(&b.map_err(|e| e.to_string())?, columns));
}
Ok(())
}
pub fn mask_batch(batch: &RecordBatch, columns: &[String]) -> Result<RecordBatch, String> {
let schema = batch.schema();
let mut fields = Vec::with_capacity(schema.fields().len());
let mut arrays: Vec<ArrayRef> = Vec::with_capacity(batch.num_columns());
for (f, a) in schema.fields().iter().zip(batch.columns()) {
if columns.contains(f.name()) {
let masked: StringArray = (0..a.len()).map(|i| (!a.is_null(i)).then_some(MASK)).collect();
fields.push(Arc::new(Field::new(f.name(), DataType::Utf8, true)));
arrays.push(Arc::new(masked));
} else {
fields.push(f.clone());
arrays.push(a.clone());
}
}
RecordBatch::try_new(Arc::new(Schema::new(fields)), arrays).map_err(|e| e.to_string())
}
pub fn masked_copy(from: &Path, to: &Path, columns: &[String]) -> Result<(), String> {
let reader =
FileReader::try_new(File::open(from).map_err(|e| e.to_string())?, None).map_err(|e| e.to_string())?;
let schema = reader.schema();
let empty = mask_batch(&RecordBatch::new_empty(schema), columns)?;
let mut w = FileWriter::try_new(File::create(to).map_err(|e| e.to_string())?, &empty.schema())
.map_err(|e| e.to_string())?;
for b in reader {
w.write(&mask_batch(&b.map_err(|e| e.to_string())?, columns)?)
.map_err(|e| e.to_string())?;
}
w.finish().map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::Int64Array;
#[test]
fn values_and_masking_keep_other_columns_and_nulls() {
let b = RecordBatch::try_from_iter([
("id", Arc::new(Int64Array::from(vec![1, 2])) as ArrayRef),
(
"pw",
Arc::new(StringArray::from(vec![Some("s3cret"), None])) as ArrayRef,
),
("pin", Arc::new(Int64Array::from(vec![123456, 7])) as ArrayRef),
])
.unwrap();
let cols = vec!["pw".to_string(), "pin".to_string(), "missing".to_string()];
assert_eq!(values(&b, &cols), vec!["s3cret", "123456", "7"]);
let m = mask_batch(&b, &cols).unwrap();
let pw = m
.column_by_name("pw")
.unwrap()
.as_any()
.downcast_ref::<StringArray>()
.unwrap();
assert_eq!(pw.value(0), MASK);
assert!(pw.is_null(1));
assert_eq!(m.column_by_name("pin").unwrap().data_type(), &DataType::Utf8);
assert_eq!(m.column_by_name("id").unwrap().data_type(), &DataType::Int64);
}
}