use super::bytes::{self, AccessorSpan};
use super::plan::{GltfScalePlan, RawAccessorTarget, plan_mismatch};
use super::{
GltfRawJsonDifference, GltfRawJsonDifferenceKind, GltfRawJsonDifferenceSummary,
GltfScaleArtifact, GltfScaleRewriteError,
};
use crate::capability::{GltfContainerKind, GltfScaleSource, raw_json_bytes};
use crate::{LoadError, load_bytes, resolve_buffers};
use animsmith_core::Property;
use animsmith_core::scale::{
ScaleCandidate, ScaleFieldDisposition, ScaleOperation, ScalePlan, ScaleProof, ScaleRewriteRule,
ScaleSourceRestField, ScaleTolerancePolicy, prove_scale,
};
use serde_json::{Map, Value};
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
const GLB_JSON_CHUNK: u32 = 0x4e4f_534a;
const GLB_BIN_CHUNK: u32 = 0x004e_4942;
const MAX_RAW_JSON_DIFFERENCES: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProofAccessorRule {
AllComponents,
Mat4TranslationColumn,
}
impl ProofAccessorRule {
fn required_accessor_type(self) -> Option<&'static str> {
match self {
Self::AllComponents => None,
Self::Mat4TranslationColumn => Some("MAT4"),
}
}
}
fn proof_scales_component(rule: ProofAccessorRule, component: usize) -> bool {
match rule {
ProofAccessorRule::AllComponents => true,
ProofAccessorRule::Mat4TranslationColumn => matches!(component, 12..=14),
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct GltfScaleArtifactProof {
pub core: ScaleProof,
pub length_factor_residual: f64,
pub dimensionless_residual: f64,
pub preserved_byte_ranges: usize,
pub rewritten_accessor_count: usize,
}
pub fn prove_rewritten_artifact(
source: &GltfScaleSource,
artifact: &GltfScaleArtifact,
plan: &ScalePlan,
) -> Result<GltfScaleArtifactProof, GltfScaleRewriteError> {
let tolerance = plan.tolerance_policy();
let ScaleOperation::WholeDocumentLinearUnits { factor } = plan.operation() else {
return Err(failed(
"plan declares a whole-document unit conversion",
1.0,
0.0,
));
};
if factor != artifact.declared_factor() {
return Err(failed(
"plan factor equals the artifact's declared factor",
(factor - artifact.declared_factor()).abs(),
0.0,
));
}
let reloaded = load_bytes(Path::new("scale-artifact"), artifact.bytes())?;
let core = prove_scale(
source.document(),
&ScaleCandidate::from_document(reloaded),
plan,
)
.map_err(GltfScaleRewriteError::Plan)?;
let (container, json_bytes) = raw_json_bytes(artifact.bytes())?;
if container != artifact.container() {
return Err(failed("artifact container kind is unchanged", 1.0, 0.0));
}
let artifact_json: Value = serde_json::from_slice(json_bytes)
.map_err(|error| LoadError::Malformed(format!("artifact JSON is invalid: {error}")))?;
let source_root = object(source.raw_json())?;
let artifact_root = object(&artifact_json)?;
let artifact_gltf = gltf::Gltf::from_slice(artifact.bytes()).map_err(LoadError::Gltf)?;
let artifact_buffers = resolve_buffers(&artifact_gltf, None)?;
check_container_integrity(artifact, artifact_root, &artifact_buffers)?;
check_array_identities(source_root, artifact_root)?;
let gltf_plan = GltfScalePlan::new(source, plan)?;
let mut proof = GltfScaleArtifactProof {
core,
length_factor_residual: 0.0,
dimensionless_residual: 0.0,
preserved_byte_ranges: 0,
rewritten_accessor_count: 0,
};
let mut converted_pointers = BTreeSet::new();
check_node_transforms(
source_root,
artifact_root,
&gltf_plan,
factor,
&tolerance,
&mut converted_pointers,
&mut proof,
)?;
let expected = scale_bearing_accessors(&gltf_plan, factor != 1.0)?;
if artifact.rewritten_accessors() != expected.keys().copied().collect::<Vec<_>>() {
return Err(failed(
"artifact reports exactly the accessors this proof independently derives",
artifact.rewritten_accessors().len() as f64,
expected.len() as f64,
));
}
proof.rewritten_accessor_count = expected.len();
let mut spans = Vec::with_capacity(expected.len());
for (&accessor_index, &rule) in &expected {
let span = bytes::accessor_span_typed(
source_root,
source.resolved_buffers(),
accessor_index,
rule.required_accessor_type(),
)?;
let artifact_span = bytes::accessor_span_typed(
artifact_root,
&artifact_buffers,
accessor_index,
rule.required_accessor_type(),
)?;
if span != artifact_span {
return Err(failed(
"converted accessors keep their source byte layout",
artifact_span.start as f64,
span.start as f64,
));
}
spans.push((span, rule));
}
check_converted_payloads(
source.resolved_buffers(),
&artifact_buffers,
&spans,
factor,
&tolerance,
&mut proof,
)?;
check_accessor_bounds(
source_root,
artifact_root,
&artifact_buffers,
&spans,
factor,
&tolerance,
&mut converted_pointers,
&mut proof,
)?;
if artifact.rewritten_json_pointers() != converted_pointers.iter().cloned().collect::<Vec<_>>()
{
return Err(failed(
"artifact reports exactly the JSON pointers this proof independently derives",
artifact.rewritten_json_pointers().len() as f64,
converted_pointers.len() as f64,
));
}
let mut allowed = converted_pointers;
for &buffer_index in artifact.reencoded_buffers() {
allowed.insert(format!("/buffers/{buffer_index}/uri"));
}
check_preserved_json(
source.raw_json(),
&artifact_json,
&allowed,
"every raw JSON location outside the converted set is preserved exactly",
)?;
let converted_spans: Vec<AccessorSpan> = spans.iter().map(|(span, _)| *span).collect();
proof.preserved_byte_ranges = check_preserved_bytes(
source.resolved_buffers(),
&artifact_buffers,
&converted_spans,
)?;
let repeat = super::rewrite_scale_plan(source, plan)?;
if repeat.bytes() != artifact.bytes() {
return Err(failed(
"rewriting the same source twice yields identical bytes",
repeat.bytes().len() as f64,
artifact.bytes().len() as f64,
));
}
Ok(proof)
}
pub(super) fn check_container_integrity(
artifact: &GltfScaleArtifact,
artifact_root: &Map<String, Value>,
artifact_buffers: &[Vec<u8>],
) -> Result<(), GltfScaleRewriteError> {
if artifact.container() == GltfContainerKind::Glb {
let bytes = artifact.bytes();
let word = |offset: usize| -> Result<u32, GltfScaleRewriteError> {
bytes
.get(offset..offset + 4)
.and_then(|slice| slice.try_into().ok())
.map(u32::from_le_bytes)
.ok_or_else(|| failed("GLB container is long enough for its own framing", 0.0, 1.0))
};
if &bytes[0..4.min(bytes.len())] != b"glTF" || word(4)? != 2 {
return Err(failed("GLB declares magic 'glTF' and version 2", 0.0, 1.0));
}
if word(8)? as usize != bytes.len() {
return Err(failed(
"GLB total length equals the emitted byte count",
f64::from(word(8)?),
bytes.len() as f64,
));
}
let json_len = word(12)? as usize;
if word(16)? != GLB_JSON_CHUNK || !json_len.is_multiple_of(4) {
return Err(failed(
"GLB JSON chunk is typed and 4-byte padded",
0.0,
1.0,
));
}
let mut offset = 20 + json_len;
let mut framed = offset;
if offset < bytes.len() {
let bin_len = word(offset)? as usize;
if word(offset + 4)? != GLB_BIN_CHUNK || !bin_len.is_multiple_of(4) {
return Err(failed("GLB BIN chunk is typed and 4-byte padded", 0.0, 1.0));
}
offset += 8;
framed = offset + bin_len;
}
if framed != bytes.len() {
return Err(failed(
"GLB chunk lengths account for every emitted byte",
framed as f64,
bytes.len() as f64,
));
}
}
let declared = artifact_root
.get("buffers")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
for (buffer_index, buffer) in declared.iter().enumerate() {
let byte_length = buffer
.get("byteLength")
.and_then(Value::as_u64)
.unwrap_or_default() as usize;
let resolved = artifact_buffers.get(buffer_index).map_or(0, Vec::len);
if resolved < byte_length {
return Err(failed(
"every declared buffer byteLength is backed by resolved bytes",
resolved as f64,
byte_length as f64,
));
}
}
Ok(())
}
pub(super) fn check_array_identities(
source_root: &Map<String, Value>,
artifact_root: &Map<String, Value>,
) -> Result<(), GltfScaleRewriteError> {
const ARRAYS: &[&str] = &[
"accessors",
"animations",
"bufferViews",
"buffers",
"cameras",
"images",
"materials",
"meshes",
"nodes",
"samplers",
"scenes",
"skins",
"textures",
];
for key in ARRAYS {
let length =
|root: &Map<String, Value>| root.get(*key).and_then(Value::as_array).map(Vec::len);
if length(source_root) != length(artifact_root) {
return Err(failed(
"every top-level array keeps its source length",
length(artifact_root).unwrap_or_default() as f64,
length(source_root).unwrap_or_default() as f64,
));
}
}
Ok(())
}
fn check_node_transforms(
source_root: &Map<String, Value>,
artifact_root: &Map<String, Value>,
plan: &GltfScalePlan,
factor: f64,
tolerance: &ScaleTolerancePolicy,
converted_pointers: &mut BTreeSet<String>,
proof: &mut GltfScaleArtifactProof,
) -> Result<(), GltfScaleRewriteError> {
let source_nodes = source_root
.get("nodes")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
let artifact_nodes = artifact_root
.get("nodes")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
for binding in plan.node_bindings() {
let node_index = binding.source_node_index;
let before = source_nodes
.get(node_index)
.ok_or_else(|| plan_mismatch("source_node_payload_missing"))?;
let after = artifact_nodes
.get(node_index)
.ok_or_else(|| plan_mismatch("artifact_node_payload_missing"))?;
for (member, field, length, scales) in [
(
"translation",
ScaleSourceRestField::Translation,
3usize,
&[0usize, 1, 2] as &[usize],
),
(
"matrix",
ScaleSourceRestField::MatrixTranslation,
16,
&[12, 13, 14],
),
] {
if (member == "translation" && !binding.translation_declared)
|| (member == "matrix" && !binding.matrix_declared)
{
continue;
}
let Some(source_values) = before.get(member).and_then(Value::as_array) else {
continue;
};
let rewrites = validate_proof_whole_document_disposition(
plan.source_rest(node_index, field)?,
factor != 1.0,
)?;
if !rewrites {
continue;
}
let pointer = format!("/nodes/{node_index}/{member}");
let artifact_values = after
.get(member)
.and_then(Value::as_array)
.filter(|values| values.len() == length && source_values.len() == length)
.ok_or_else(|| {
failed(
"a converted node transform keeps its authored arity",
0.0,
length as f64,
)
})?;
for component in 0..length {
let before = numeric(&source_values[component], &pointer)?;
let after = numeric(&artifact_values[component], &pointer)?;
if scales.contains(&component) {
track_length(before, after, factor, tolerance, proof)?;
} else {
track_dimensionless(before, after, proof)?;
}
}
converted_pointers.insert(pointer);
}
}
Ok(())
}
fn check_converted_payloads(
source_buffers: &[Vec<u8>],
artifact_buffers: &[Vec<u8>],
spans: &[(AccessorSpan, ProofAccessorRule)],
factor: f64,
tolerance: &ScaleTolerancePolicy,
proof: &mut GltfScaleArtifactProof,
) -> Result<(), GltfScaleRewriteError> {
for &(span, rule) in spans {
let before = bytes::read_span(source_buffers, span);
let after = bytes::read_span(artifact_buffers, span);
if before.len() != after.len() {
return Err(failed(
"a converted accessor keeps its element count",
after.len() as f64,
before.len() as f64,
));
}
for (index, (&before, &after)) in before.iter().zip(&after).enumerate() {
if proof_scales_component(rule, index % span.components) {
let expected = f64::from(before) * factor;
if after.to_bits() != (expected as f32).to_bits() {
return Err(failed(
"every converted element is the single narrowing of before * q",
f64::from(after),
expected,
));
}
track_length(
f64::from(before),
f64::from(after),
factor,
tolerance,
proof,
)?;
} else if after.to_bits() != before.to_bits() {
return Err(failed(
"a converted accessor's dimensionless components are bit-identical",
f64::from(after),
f64::from(before),
));
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn check_accessor_bounds(
source_root: &Map<String, Value>,
artifact_root: &Map<String, Value>,
artifact_buffers: &[Vec<u8>],
spans: &[(AccessorSpan, ProofAccessorRule)],
factor: f64,
tolerance: &ScaleTolerancePolicy,
converted_pointers: &mut BTreeSet<String>,
proof: &mut GltfScaleArtifactProof,
) -> Result<(), GltfScaleRewriteError> {
for &(span, rule) in spans {
let payload = bytes::read_span(artifact_buffers, span);
for (member, is_min) in [("min", true), ("max", false)] {
let pointer = format!("/accessors/{}/{member}", span.accessor_index);
let Some(source_bounds) = source_root
.get("accessors")
.and_then(Value::as_array)
.and_then(|accessors| accessors.get(span.accessor_index))
.and_then(|accessor| accessor.get(member))
.and_then(Value::as_array)
else {
continue;
};
let artifact_bounds = artifact_root
.get("accessors")
.and_then(Value::as_array)
.and_then(|accessors| accessors.get(span.accessor_index))
.and_then(|accessor| accessor.get(member))
.and_then(Value::as_array)
.filter(|bounds| bounds.len() == source_bounds.len())
.ok_or_else(|| {
failed(
"a converted accessor keeps its authored bound arity",
0.0,
source_bounds.len() as f64,
)
})?;
for component in 0..source_bounds.len() {
let before = numeric(&source_bounds[component], &pointer)?;
let after = numeric(&artifact_bounds[component], &pointer)?;
if !proof_scales_component(rule, component) {
track_dimensionless(before, after, proof)?;
continue;
}
track_length(before, after, factor, tolerance, proof)?;
let observed = payload
.iter()
.skip(component)
.step_by(span.components)
.copied()
.fold(
if is_min {
f32::INFINITY
} else {
f32::NEG_INFINITY
},
|accumulator, value| {
if is_min {
accumulator.min(value)
} else {
accumulator.max(value)
}
},
);
let declared = after as f32;
let bounds_data = if is_min {
declared <= observed
} else {
declared >= observed
};
if !bounds_data {
return Err(failed(
"a converted bound still bounds the converted payload",
f64::from(declared),
f64::from(observed),
));
}
}
converted_pointers.insert(pointer);
}
}
Ok(())
}
pub(super) fn check_preserved_bytes(
source_buffers: &[Vec<u8>],
artifact_buffers: &[Vec<u8>],
spans: &[AccessorSpan],
) -> Result<usize, GltfScaleRewriteError> {
if source_buffers.len() != artifact_buffers.len() {
return Err(failed(
"the artifact declares the same number of resolved buffers",
artifact_buffers.len() as f64,
source_buffers.len() as f64,
));
}
let mut preserved = 0usize;
for (buffer_index, (before, after)) in source_buffers.iter().zip(artifact_buffers).enumerate() {
if before.len() != after.len() {
return Err(failed(
"every resolved buffer keeps its source byte length",
after.len() as f64,
before.len() as f64,
));
}
let mut converted: Vec<(usize, usize)> = spans
.iter()
.filter(|span| span.buffer == buffer_index)
.map(|span| (span.start, span.end))
.collect();
converted.sort_unstable();
let mut cursor = 0usize;
for (start, end) in converted.into_iter().chain([(before.len(), before.len())]) {
if cursor < start {
if before[cursor..start] != after[cursor..start] {
return Err(failed(
"buffer bytes outside the converted ranges are preserved",
cursor as f64,
start as f64,
));
}
preserved += 1;
}
cursor = cursor.max(end);
}
}
Ok(preserved)
}
#[derive(Debug, Default)]
struct RawJsonDifferenceCollector {
differences: Vec<GltfRawJsonDifference>,
total: usize,
}
impl RawJsonDifferenceCollector {
fn record(&mut self, pointer: String, kind: GltfRawJsonDifferenceKind) {
self.total += 1;
if self.differences.len() < MAX_RAW_JSON_DIFFERENCES {
self.differences
.push(GltfRawJsonDifference { pointer, kind });
}
}
fn finish(self) -> GltfRawJsonDifferenceSummary {
let omitted = self.total - self.differences.len();
GltfRawJsonDifferenceSummary {
differences: self.differences,
omitted,
}
}
}
pub(super) fn check_preserved_json(
before: &Value,
after: &Value,
allowed: &BTreeSet<String>,
claim: &'static str,
) -> Result<(), GltfScaleRewriteError> {
let mut collector = RawJsonDifferenceCollector::default();
collect_json_differences(before, after, "", allowed, &mut collector);
if collector.total == 0 {
return Ok(());
}
let total = collector.total;
let summary = collector.finish();
Err(GltfScaleRewriteError::ArtifactProofFailed {
claim,
observed: total as f64,
tolerance: 0.0,
raw_json_differences: Some(summary),
})
}
fn collect_json_differences(
before: &Value,
after: &Value,
pointer: &str,
allowed: &BTreeSet<String>,
out: &mut RawJsonDifferenceCollector,
) {
if allowed.contains(pointer) {
return;
}
match (before, after) {
(Value::Object(before), Value::Object(after)) => {
let keys: BTreeSet<&String> = before.keys().chain(after.keys()).collect();
for key in keys {
let child = format!("{pointer}/{}", key.replace('~', "~0").replace('/', "~1"));
match (before.get(key), after.get(key)) {
(Some(before), Some(after)) => {
collect_json_differences(before, after, &child, allowed, out);
}
_ if allowed.contains(&child) => {}
(None, Some(_)) => {
out.record(child, GltfRawJsonDifferenceKind::ArtifactAdded);
}
(Some(_), None) => {
out.record(child, GltfRawJsonDifferenceKind::ArtifactRemoved);
}
(None, None) => unreachable!("a key came from at least one object"),
}
}
}
(Value::Array(before), Value::Array(after)) if before.len() == after.len() => {
for (index, (before, after)) in before.iter().zip(after).enumerate() {
collect_json_differences(
before,
after,
&format!("{pointer}/{index}"),
allowed,
out,
);
}
}
(Value::Number(before), Value::Number(after)) => {
if !json_numbers_have_identical_value_and_zero_sign(before, after) {
out.record(pointer.to_owned(), GltfRawJsonDifferenceKind::ValueChanged);
}
}
(before, after) if before == after => {}
_ => out.record(pointer.to_owned(), GltfRawJsonDifferenceKind::ValueChanged),
}
}
fn json_numbers_have_identical_value_and_zero_sign(
before: &serde_json::Number,
after: &serde_json::Number,
) -> bool {
if before != after {
return false;
}
match (before.as_f64(), after.as_f64()) {
(Some(before), Some(after)) if before == 0.0 && after == 0.0 => {
before.to_bits() == after.to_bits()
}
_ => true,
}
}
fn scale_bearing_accessors(
plan: &GltfScalePlan,
factor_changes: bool,
) -> Result<BTreeMap<usize, ProofAccessorRule>, GltfScaleRewriteError> {
let mut out = BTreeMap::new();
for binding in plan.accessor_bindings() {
let rule = match &binding.target {
RawAccessorTarget::MeshPositions { disposition } => {
validate_proof_whole_document_disposition(*disposition, factor_changes)?
.then_some(ProofAccessorRule::AllComponents)
}
RawAccessorTarget::MorphPositions => {
factor_changes.then_some(ProofAccessorRule::AllComponents)
}
RawAccessorTarget::InstanceInverseBind { source_skin_index } => {
let skin = plan.skin_binding(*source_skin_index)?;
let mut rewrite = None;
for slot in &skin.slots {
let slot_rewrite = validate_proof_whole_document_disposition(
slot.disposition
.ok_or_else(|| plan_mismatch("inverse_bind_disposition_missing"))?,
factor_changes,
)?;
match rewrite {
Some(previous) if previous != slot_rewrite => {
return Err(plan_mismatch("mixed_whole_document_accessor_disposition"));
}
Some(_) => {}
None => rewrite = Some(slot_rewrite),
}
}
rewrite
.unwrap_or(false)
.then_some(ProofAccessorRule::Mat4TranslationColumn)
}
RawAccessorTarget::Animation {
property: Property::Translation,
disposition,
..
} => validate_proof_whole_document_disposition(*disposition, factor_changes)?
.then_some(ProofAccessorRule::AllComponents),
RawAccessorTarget::MeshNormals { .. } => None,
RawAccessorTarget::PreserveExact | RawAccessorTarget::Animation { .. } => None,
};
if let Some(rule) = rule {
out.insert(binding.accessor_index, rule);
}
}
Ok(out)
}
fn validate_proof_whole_document_disposition(
disposition: ScaleFieldDisposition,
factor_changes: bool,
) -> Result<bool, GltfScaleRewriteError> {
match (factor_changes, disposition) {
(true, ScaleFieldDisposition::Rewrite(ScaleRewriteRule::WholeDocumentLength)) => Ok(true),
(false, ScaleFieldDisposition::PreserveExact) => Ok(false),
_ => Err(plan_mismatch("invalid_whole_document_field_disposition")),
}
}
pub(super) fn track_length(
before: f64,
after: f64,
factor: f64,
tolerance: &ScaleTolerancePolicy,
proof: &mut GltfScaleArtifactProof,
) -> Result<(), GltfScaleRewriteError> {
let expected = before * factor;
let residual = (after - expected).abs();
proof.length_factor_residual = proof.length_factor_residual.max(residual);
let bound = tolerance.scalar_tolerance(expected, after);
if residual > bound {
return Err(failed(
"every converted length differs from the source by exactly the declared factor",
residual,
bound,
));
}
Ok(())
}
pub(super) fn track_dimensionless(
before: f64,
after: f64,
proof: &mut GltfScaleArtifactProof,
) -> Result<(), GltfScaleRewriteError> {
let residual = (after - before).abs();
proof.dimensionless_residual = proof.dimensionless_residual.max(residual);
if before.to_bits() != after.to_bits() {
return Err(failed(
"every dimensionless value inside a converted range is invariant",
residual,
0.0,
));
}
Ok(())
}
pub(super) fn failed(claim: &'static str, observed: f64, tolerance: f64) -> GltfScaleRewriteError {
GltfScaleRewriteError::ArtifactProofFailed {
claim,
observed,
tolerance,
raw_json_differences: None,
}
}
pub(super) fn object(value: &Value) -> Result<&Map<String, Value>, GltfScaleRewriteError> {
value
.as_object()
.ok_or_else(|| LoadError::Malformed("top-level glTF JSON is not an object".into()).into())
}
pub(super) fn numeric(value: &Value, location: &str) -> Result<f64, GltfScaleRewriteError> {
value
.as_f64()
.ok_or_else(|| LoadError::Malformed(format!("{location} is not a number")).into())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::preflight_scale_source_bytes;
use animsmith_core::scale::{ScaleOperation, ScaleRequest, plan_scale};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use serde_json::json;
mod offsets {
pub const POSITION: usize = 0; pub const INVERSE_BIND: usize = 36; pub const JOINTS: usize = 100; pub const WEIGHTS: usize = 124; pub const SPARE: usize = 172; pub const LENGTH: usize = 176;
}
const FACTOR: f64 = 4.0;
const POSITIONS: [f32; 9] = [1.0, 2.0, -3.0, 0.5, -0.25, 4.0, 2.0, 0.0, 1.5];
const INVERSE_BIND: [f32; 16] = [
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 2.0, -0.5, 1.0,
];
fn data_uri(bytes: &[u8]) -> String {
format!(
"data:application/octet-stream;base64,{}",
STANDARD.encode(bytes)
)
}
fn fixture_buffer() -> Vec<u8> {
let mut buffer = vec![0u8; offsets::LENGTH];
for (index, value) in POSITIONS.iter().enumerate() {
let at = offsets::POSITION + index * 4;
buffer[at..at + 4].copy_from_slice(&value.to_le_bytes());
}
for (index, value) in INVERSE_BIND.iter().enumerate() {
let at = offsets::INVERSE_BIND + index * 4;
buffer[at..at + 4].copy_from_slice(&value.to_le_bytes());
}
for vertex in 0..3 {
let at = offsets::WEIGHTS + vertex * 16;
buffer[at..at + 4].copy_from_slice(&1.0f32.to_le_bytes());
}
buffer
}
fn fixture_json(buffer: &[u8]) -> Value {
json!({
"asset": { "version": "2.0" },
"buffers": [{ "uri": data_uri(buffer), "byteLength": offsets::LENGTH }],
"bufferViews": [
{ "buffer": 0, "byteOffset": offsets::POSITION, "byteLength": 36 },
{ "buffer": 0, "byteOffset": offsets::INVERSE_BIND, "byteLength": 64 },
{ "buffer": 0, "byteOffset": offsets::JOINTS, "byteLength": 24 },
{ "buffer": 0, "byteOffset": offsets::WEIGHTS, "byteLength": 48 }
],
"accessors": [
{ "bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3",
"min": [0.5, -0.25, -3.0], "max": [2.0, 2.0, 4.0] },
{ "bufferView": 1, "componentType": 5126, "count": 1, "type": "MAT4" },
{ "bufferView": 2, "componentType": 5123, "count": 3, "type": "VEC4" },
{ "bufferView": 3, "componentType": 5126, "count": 3, "type": "VEC4" }
],
"materials": [{ "name": "surface" }],
"meshes": [{ "primitives": [{
"attributes": { "POSITION": 0, "JOINTS_0": 2, "WEIGHTS_0": 3 },
"material": 0
}] }],
"nodes": [{ "name": "joint" }, { "name": "holder", "mesh": 0, "skin": 0 }],
"scenes": [{ "nodes": [0, 1] }],
"scene": 0,
"skins": [{ "joints": [0], "skeleton": 0, "inverseBindMatrices": 1 }]
})
}
fn fixture() -> (GltfScaleSource, GltfScaleArtifact, ScalePlan) {
let value = fixture_json(&fixture_buffer());
fixture_from_value(&value)
}
fn fixture_from_value(value: &Value) -> (GltfScaleSource, GltfScaleArtifact, ScalePlan) {
fixture_from_value_with_factor(value, FACTOR)
}
fn fixture_from_value_with_factor(
value: &Value,
factor: f64,
) -> (GltfScaleSource, GltfScaleArtifact, ScalePlan) {
let bytes = serde_json::to_vec(&value).expect("fixture serializes");
let source = preflight_scale_source_bytes(Path::new("proof-fixture.gltf"), &bytes)
.expect("the fixture preflights cleanly");
let plan = plan_scale(&ScaleRequest {
operation: ScaleOperation::WholeDocumentLinearUnits { factor },
document: source.document(),
capability: &super::super::capability_facts(source.manifest()),
})
.expect("plan");
let artifact = super::super::rewrite_linear_units(&source, factor).expect("rewrite");
(source, artifact, plan)
}
fn artifact_value(artifact: &GltfScaleArtifact) -> Value {
serde_json::from_slice(artifact.bytes()).expect("a .gltf artifact is JSON")
}
fn put_artifact_value(artifact: &mut GltfScaleArtifact, value: &Value) {
artifact.bytes = serde_json::to_vec(value).expect("corrupted fixture serializes");
}
fn artifact_buffer(value: &Value) -> Vec<u8> {
let uri = value["buffers"][0]["uri"].as_str().expect("data URI");
STANDARD
.decode(uri.split_once("base64,").expect("base64 data URI").1)
.expect("valid base64")
}
fn put_artifact_buffer(value: &mut Value, bytes: &[u8]) {
value["buffers"][0]["uri"] = json!(data_uri(bytes));
}
fn expect_claim(
source: &GltfScaleSource,
artifact: &GltfScaleArtifact,
plan: &ScalePlan,
expected: &str,
) {
match prove_rewritten_artifact(source, artifact, plan) {
Err(GltfScaleRewriteError::ArtifactProofFailed {
claim,
raw_json_differences,
..
}) => {
assert_eq!(claim, expected);
assert_eq!(
raw_json_differences, None,
"ordinary proof claims do not carry raw JSON diagnostics"
);
}
other => panic!("expected the claim {expected:?} to fail, got {other:?}"),
}
}
#[test]
fn the_uncorrupted_fixture_proves_and_reports_its_evidence() {
let (source, artifact, plan) = fixture();
let proof = prove_rewritten_artifact(&source, &artifact, &plan).expect("artifact proof");
assert_eq!(proof.rewritten_accessor_count, 2, "POSITION and the IBM");
assert_eq!(proof.length_factor_residual, 0.0);
assert_eq!(proof.dimensionless_residual, 0.0);
assert_eq!(proof.preserved_byte_ranges, 1);
assert_eq!(artifact.rewritten_accessors(), [0, 1]);
assert_eq!(
artifact.rewritten_json_pointers(),
["/accessors/0/max", "/accessors/0/min"]
);
}
#[test]
fn a_flipped_byte_outside_every_converted_range_fails_byte_preservation() {
let (source, mut artifact, plan) = fixture();
let mut value = artifact_value(&artifact);
let mut buffer = artifact_buffer(&value);
buffer[offsets::SPARE] ^= 0xff;
put_artifact_buffer(&mut value, &buffer);
put_artifact_value(&mut artifact, &value);
expect_claim(
&source,
&artifact,
&plan,
"buffer bytes outside the converted ranges are preserved",
);
}
#[test]
fn a_dimensionless_component_inside_a_converted_range_must_be_bit_identical() {
let (source, mut artifact, plan) = fixture();
let mut value = artifact_value(&artifact);
let mut buffer = artifact_buffer(&value);
const SIGN_BYTE: usize = offsets::INVERSE_BIND + 4 + 3;
buffer[SIGN_BYTE] |= 0x80;
put_artifact_buffer(&mut value, &buffer);
put_artifact_value(&mut artifact, &value);
expect_claim(
&source,
&artifact,
&plan,
"a converted accessor's dimensionless components are bit-identical",
);
}
#[test]
fn a_dimensionless_node_matrix_component_must_be_exact_in_parsed_f64() {
let mut value = fixture_json(&fixture_buffer());
value["nodes"][0] = json!({
"name": "joint",
"matrix": [
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
]
});
let (source, mut artifact, plan) = fixture_from_value(&value);
let mut doctored = artifact_value(&artifact);
let adjacent = f64::from_bits(1.0f64.to_bits() + 1);
doctored["nodes"][0]["matrix"][0] = json!(adjacent);
put_artifact_value(&mut artifact, &doctored);
let error = prove_rewritten_artifact(&source, &artifact, &plan)
.expect_err("an adjacent identity-multiplier matrix value must be refused");
match error {
GltfScaleRewriteError::ArtifactProofFailed {
claim, observed, ..
} => {
assert_eq!(
claim,
"every dimensionless value inside a converted range is invariant"
);
assert_eq!(observed, adjacent - 1.0, "one adjacent matrix f64");
}
other => panic!("expected a dimensionless matrix residual, got {other:?}"),
}
}
#[test]
fn a_factor_one_matrix_translation_is_preserved_by_the_public_artifact_proof() {
let mut value = fixture_json(&fixture_buffer());
let authored = f64::from_bits(1.0f64.to_bits() + 1);
value["nodes"][1]["matrix"] = json!([
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, authored, 0.0, 0.0, 1.0
]);
let (source, mut artifact, plan) = fixture_from_value_with_factor(&value, 1.0);
assert!(artifact.rewritten_accessors().is_empty());
assert!(artifact.rewritten_json_pointers().is_empty());
assert_eq!(
artifact_value(&artifact)["nodes"][1]["matrix"][12]
.as_f64()
.expect("numeric matrix translation")
.to_bits(),
authored.to_bits()
);
let proof =
prove_rewritten_artifact(&source, &artifact, &plan).expect("factor-one artifact proof");
assert_eq!(proof.rewritten_accessor_count, 0);
let mut doctored = artifact_value(&artifact);
let adjacent = f64::from_bits(authored.to_bits() + 1);
doctored["nodes"][1]["matrix"][12] = json!(adjacent);
put_artifact_value(&mut artifact, &doctored);
let error = prove_rewritten_artifact(&source, &artifact, &plan)
.expect_err("a changed factor-one matrix translation must be refused");
match error {
GltfScaleRewriteError::ArtifactProofFailed {
claim,
observed,
raw_json_differences: Some(summary),
..
} => {
assert_eq!(
claim,
"every raw JSON location outside the converted set is preserved exactly"
);
assert_eq!(observed, 1.0);
assert_eq!(summary.differences.len(), 1);
assert_eq!(summary.differences[0].pointer, "/nodes/1/matrix/12");
assert_eq!(
summary.differences[0].kind,
GltfRawJsonDifferenceKind::ValueChanged
);
}
other => panic!("expected a located raw JSON difference, got {other:?}"),
}
}
#[test]
fn an_under_reported_converted_accessor_fails_the_accessor_cross_check() {
let (source, mut artifact, plan) = fixture();
artifact.rewritten_accessors.pop();
expect_claim(
&source,
&artifact,
&plan,
"artifact reports exactly the accessors this proof independently derives",
);
}
#[test]
fn an_under_reported_rewritten_json_pointer_fails_the_pointer_cross_check() {
let (source, mut artifact, plan) = fixture();
artifact.rewritten_json_pointers.pop();
expect_claim(
&source,
&artifact,
&plan,
"artifact reports exactly the JSON pointers this proof independently derives",
);
}
#[test]
fn added_and_removed_preserved_json_locations_keep_whole_document_direction() {
let (source, mut artifact, plan) = fixture();
let mut value = artifact_value(&artifact);
let material = value["materials"][0]
.as_object_mut()
.expect("fixture material is an object");
material.remove("name");
material.insert("replacement".into(), json!(true));
put_artifact_value(&mut artifact, &value);
let error = prove_rewritten_artifact(&source, &artifact, &plan)
.expect_err("changed preserved JSON must fail");
assert_eq!(
error.to_string(),
"artifact proof claim \"every raw JSON location outside the converted set is preserved exactly\" observed 2, tolerance 0; raw JSON differences: /materials/0/name (artifact-removed), /materials/0/replacement (artifact-added)"
);
match error {
GltfScaleRewriteError::ArtifactProofFailed {
claim,
observed,
tolerance,
raw_json_differences: Some(summary),
} => {
assert_eq!(
claim,
"every raw JSON location outside the converted set is preserved exactly"
);
assert_eq!(observed, 2.0);
assert_eq!(tolerance, 0.0);
assert_eq!(
summary,
GltfRawJsonDifferenceSummary {
differences: vec![
GltfRawJsonDifference {
pointer: "/materials/0/name".into(),
kind: GltfRawJsonDifferenceKind::ArtifactRemoved,
},
GltfRawJsonDifference {
pointer: "/materials/0/replacement".into(),
kind: GltfRawJsonDifferenceKind::ArtifactAdded,
},
],
omitted: 0,
}
);
}
other => panic!("expected located JSON diagnostics, got {other:?}"),
}
}
#[test]
fn json_difference_collection_is_typed_escaped_ordered_and_allowlisted() {
let before = json!({
"a~/b~/c": 1,
"allowed": "source secret",
"allowedly": 1,
"removed": true,
"value": 1
});
let after = json!({
"a~/b~/c": 2,
"added": true,
"allowed": "artifact secret",
"allowedly": 2,
"value": 2
});
let allowed = BTreeSet::from(["/allowed".to_owned()]);
let mut collector = RawJsonDifferenceCollector::default();
collect_json_differences(&before, &after, "", &allowed, &mut collector);
assert_eq!(
collector.finish(),
GltfRawJsonDifferenceSummary {
differences: vec![
GltfRawJsonDifference {
pointer: "/added".into(),
kind: GltfRawJsonDifferenceKind::ArtifactAdded,
},
GltfRawJsonDifference {
pointer: "/allowedly".into(),
kind: GltfRawJsonDifferenceKind::ValueChanged,
},
GltfRawJsonDifference {
pointer: "/a~0~1b~0~1c".into(),
kind: GltfRawJsonDifferenceKind::ValueChanged,
},
GltfRawJsonDifference {
pointer: "/removed".into(),
kind: GltfRawJsonDifferenceKind::ArtifactRemoved,
},
GltfRawJsonDifference {
pointer: "/value".into(),
kind: GltfRawJsonDifferenceKind::ValueChanged,
},
],
omitted: 0,
}
);
}
#[test]
fn json_difference_collection_preserves_zero_sign_and_quaternion_sign() {
let before: Value = serde_json::from_str(
r#"{"extras":{"authoredZero":-0.0},"nodes":[{"rotation":[0.0,0.0,0.0,-1.0]}]}"#,
)
.expect("source JSON");
let after: Value = serde_json::from_str(
r#"{"extras":{"authoredZero":0.0},"nodes":[{"rotation":[0.0,0.0,0.0,1.0]}]}"#,
)
.expect("artifact JSON");
assert_eq!(
before["extras"]["authoredZero"]
.as_f64()
.expect("source zero")
.to_bits(),
(-0.0f64).to_bits()
);
let mut collector = RawJsonDifferenceCollector::default();
collect_json_differences(&before, &after, "", &BTreeSet::new(), &mut collector);
assert_eq!(
collector.finish(),
GltfRawJsonDifferenceSummary {
differences: vec![
GltfRawJsonDifference {
pointer: "/extras/authoredZero".into(),
kind: GltfRawJsonDifferenceKind::ValueChanged,
},
GltfRawJsonDifference {
pointer: "/nodes/0/rotation/3".into(),
kind: GltfRawJsonDifferenceKind::ValueChanged,
},
],
omitted: 0,
}
);
}
#[test]
fn json_difference_collection_caps_storage_but_counts_every_difference() {
let mut before = Map::new();
let mut after = Map::new();
for index in 0..20 {
let key = format!("key-{index:02}");
match index % 3 {
0 => {
before.insert(key.clone(), json!(0));
after.insert(key, json!(1));
}
1 => {
after.insert(key, json!(1));
}
_ => {
before.insert(key, json!(0));
}
}
}
let error = check_preserved_json(
&Value::Object(before),
&Value::Object(after),
&BTreeSet::new(),
"the capped preservation fixture stays exact",
)
.expect_err("twenty preserved-location differences must fail");
let GltfScaleRewriteError::ArtifactProofFailed {
claim,
observed,
tolerance,
raw_json_differences: Some(summary),
} = error
else {
panic!("expected located JSON diagnostics, got {error:?}");
};
assert_eq!(claim, "the capped preservation fixture stays exact");
assert_eq!(observed, 20.0, "observed retains the full count");
assert_eq!(tolerance, 0.0);
assert_eq!(summary.differences.len() + summary.omitted, 20);
assert_eq!(summary.omitted, 4);
assert_eq!(
summary.differences,
(0..MAX_RAW_JSON_DIFFERENCES)
.map(|index| GltfRawJsonDifference {
pointer: format!("/key-{index:02}"),
kind: match index % 3 {
0 => GltfRawJsonDifferenceKind::ValueChanged,
1 => GltfRawJsonDifferenceKind::ArtifactAdded,
_ => GltfRawJsonDifferenceKind::ArtifactRemoved,
},
})
.collect::<Vec<_>>()
);
let display = super::super::RawJsonDifferenceSuffix(Some(&summary)).to_string();
assert!(display.ends_with("; 4 omitted"));
assert!(display.contains("/key-15 (value-changed)"));
assert!(
!display.contains("/key-16"),
"omitted pointers stay omitted"
);
}
#[test]
fn unequal_arrays_report_one_value_change_at_the_array_root() {
for (before, after) in [
(json!({ "nodes": [1] }), json!({ "nodes": [1, 2] })),
(json!({ "nodes": [1, 2] }), json!({ "nodes": [1] })),
] {
let mut collector = RawJsonDifferenceCollector::default();
collect_json_differences(&before, &after, "", &BTreeSet::new(), &mut collector);
assert_eq!(
collector.finish(),
GltfRawJsonDifferenceSummary {
differences: vec![GltfRawJsonDifference {
pointer: "/nodes".into(),
kind: GltfRawJsonDifferenceKind::ValueChanged,
}],
omitted: 0,
}
);
}
}
#[test]
fn a_changed_top_level_array_length_fails_array_identity() {
let (source, mut artifact, plan) = fixture();
let mut value = artifact_value(&artifact);
value["materials"]
.as_array_mut()
.expect("materials array")
.push(json!({ "name": "smuggled" }));
put_artifact_value(&mut artifact, &value);
expect_claim(
&source,
&artifact,
&plan,
"every top-level array keeps its source length",
);
}
#[test]
fn a_resolved_buffer_that_grew_fails_the_buffer_length_claim() {
let (source, mut artifact, plan) = fixture();
let mut value = artifact_value(&artifact);
let mut buffer = artifact_buffer(&value);
buffer.extend_from_slice(&[0u8; 4]);
put_artifact_buffer(&mut value, &buffer);
put_artifact_value(&mut artifact, &value);
expect_claim(
&source,
&artifact,
&plan,
"every resolved buffer keeps its source byte length",
);
}
#[test]
fn a_declared_buffer_length_without_backing_bytes_fails_container_integrity() {
let (source, mut artifact, plan) = fixture();
let mut value = artifact_value(&artifact);
value["buffers"][0]["byteLength"] = json!(offsets::LENGTH + 4);
put_artifact_value(&mut artifact, &value);
expect_claim(
&source,
&artifact,
&plan,
"every declared buffer byteLength is backed by resolved bytes",
);
}
#[test]
fn a_flipped_container_kind_fails_the_container_claim() {
let (source, mut artifact, plan) = fixture();
artifact.container = GltfContainerKind::Glb;
expect_claim(
&source,
&artifact,
&plan,
"artifact container kind is unchanged",
);
}
#[test]
fn a_bound_that_no_longer_bounds_the_converted_payload_fails() {
let (source, mut artifact, plan) = fixture();
let mut value = artifact_value(&artifact);
value["accessors"][0]["min"][0] = json!(2.0000002f32 as f64);
put_artifact_value(&mut artifact, &value);
expect_claim(
&source,
&artifact,
&plan,
"a converted bound still bounds the converted payload",
);
}
#[test]
fn bytes_that_are_not_the_rewriters_own_output_fail_the_determinism_claim() {
let (source, mut artifact, plan) = fixture();
let value = artifact_value(&artifact);
artifact.bytes = serde_json::to_vec_pretty(&value).expect("pretty serializes");
expect_claim(
&source,
&artifact,
&plan,
"rewriting the same source twice yields identical bytes",
);
}
#[test]
fn a_nested_converted_range_does_not_rewind_the_preserved_byte_cursor() {
let mut before = vec![0u8; 20];
let mut after = vec![0u8; 20];
before[10] = 1;
after[10] = 2;
let span = |start: usize, end: usize| AccessorSpan {
accessor_index: 0,
buffer: 0,
start,
end,
components: 1,
};
let spans = [span(0, 16), span(4, 8)];
let preserved = check_preserved_bytes(&[before], &[after], &spans)
.expect("only 16..20 lies outside the converted ranges");
assert_eq!(preserved, 1);
}
}