use super::cell::slice;
use super::decode::Row;
use super::{Error, Result, sys};
use crate::contract::schema::FieldDef;
impl Row<'_> {
pub(super) fn validate(self) -> Result<()> {
let Some((present, values, nvalues)) = self.wire_cells() else {
return Ok(());
};
if self.schema_id() == 0 {
return Err(Error::Decode(
"row names a schema this build does not declare (rebuild the bindings against \
the engine's contract)"
.to_owned(),
));
}
if nvalues != 0 && values.is_null() {
return Err(self.drift("row", "declares values but carries a null array"));
}
let raw = unsafe { slice(values, nvalues) };
for (i, field) in self.fields().iter().enumerate().take(nvalues) {
if present & (1u64 << i) == 0 {
continue;
}
let value = &raw[i];
if value.tag != field.tag {
return Err(self.drift(
field.name,
&format!(
"wire tag {} disagrees with the declared {}",
value.tag, field.tag
),
));
}
self.reachable(field, value)?;
}
Ok(())
}
fn reachable(self, field: &FieldDef, value: &sys::Value) -> Result<()> {
match field.tag {
sys::VAL_TEXT => {
utf8(value.ptr.cast::<u8>(), value.len).map_err(|e| self.drift(field.name, &e))?;
},
sys::VAL_TEXTS => {
for t in unsafe { slice(value.ptr.cast::<sys::Text>(), value.len) } {
utf8(t.ptr, t.len).map_err(|e| self.drift(field.name, &e))?;
}
},
sys::VAL_ROWS => {
for r in unsafe { slice(value.ptr.cast::<sys::Row>(), value.len) } {
if r.schema_id != field.nested {
return Err(self.drift(
field.name,
&format!(
"nested row is schema {} where {} is declared",
r.schema_id, field.nested
),
));
}
Row::wire_unchecked(r).validate()?;
}
},
_ => {},
}
Ok(())
}
fn drift(self, field: &str, what: &str) -> Error {
Error::Decode(format!("{}.{field}: {what}", self.schema_name()))
}
}
fn utf8<'a>(ptr: *const u8, len: usize) -> std::result::Result<&'a str, String> {
let bytes = unsafe { slice(ptr, len) };
std::str::from_utf8(bytes).map_err(|e| format!("text is not UTF-8 ({e})"))
}
#[cfg(test)]
mod tests {
use super::super::fixture::{self, all, enumeration, int, real, row, schema_id, text};
use super::*;
#[test]
fn a_tag_that_contradicts_the_declaration_is_rejected() {
let id = schema_id("similar");
let values = [text("a.rs"), int(3), enumeration(0), enumeration(0)];
let err = unsafe { Row::from_wire(&row(id, all(values.len()), &values)) }
.expect_err("a tag disagreement must not decode");
let msg = err.to_string();
assert!(msg.contains("similar.distance"), "{msg}");
assert!(msg.contains("disagrees"), "{msg}");
}
#[test]
fn a_nested_row_of_the_wrong_schema_is_rejected() {
let family = schema_id("family");
let site_values = [text("a.rs"), int(1)];
let wrong = [row(schema_id("site"), 0b11, &site_values)];
let values = [
int(1),
enumeration(0),
enumeration(0),
real(0.2),
int(80),
real(0.9),
fixture::rows(&wrong),
];
let err = unsafe { Row::from_wire(&row(family, all(values.len()), &values)) }
.expect_err("a mis-schema'd nested row must not decode");
assert!(err.to_string().contains("family.members"), "{err}");
}
#[test]
fn a_nested_row_is_checked_as_deeply_as_its_parent() {
let region = schema_id("region");
let family = schema_id("family");
let member_values = [text("a.rs"), real(1.0), int(20)];
let members = [row(region, all(member_values.len()), &member_values)];
let values = [
int(1),
enumeration(0),
enumeration(0),
real(0.2),
int(80),
real(0.9),
fixture::rows(&members),
];
let err = unsafe { Row::from_wire(&row(family, all(values.len()), &values)) }
.expect_err("recursion must reach the nested field");
assert!(err.to_string().contains("region.line_start"), "{err}");
}
#[test]
fn non_utf8_text_is_a_failure_not_a_silent_replacement() {
let id = schema_id("region");
let bad = [0x66u8, 0xff, 0x66];
let values = [
sys::Value {
tag: sys::VAL_TEXT,
reserved: 0,
integer: 0,
real: 0.0,
ptr: bad.as_ptr().cast(),
len: bad.len(),
},
int(1),
int(2),
];
let err = unsafe { Row::from_wire(&row(id, 0b111, &values)) }
.expect_err("invalid UTF-8 must not decode");
assert!(err.to_string().contains("region.path"), "{err}");
}
#[test]
fn a_schema_this_build_does_not_declare_is_named_not_guessed() {
let beyond = u32::try_from(crate::contract::schema::SCHEMAS.len()).unwrap_or(u32::MAX) + 1;
let err = unsafe { Row::from_wire(&row(beyond, 0, &[])) }
.expect_err("an unknown schema id must not decode");
assert!(err.to_string().contains("does not declare"), "{err}");
}
#[test]
fn an_absent_field_is_not_validated_at_all() {
let id = schema_id("similar");
let values = [text("a.rs"), int(3), enumeration(0), enumeration(0)];
let decoded = unsafe { Row::from_wire(&row(id, all(values.len()) & !0b10, &values)) }
.expect("an absent cell is never inspected");
assert_eq!(decoded.real("distance"), None);
}
}