use super::GltfScaleRewriteError;
use crate::capability::{GltfBufferSourceKind, GltfCapabilityManifest, GltfContainerKind};
use crate::write::{glb_len_u32, plan_glb_lengths};
use crate::{LoadError, WriteError};
use base64::Engine as _;
use serde_json::Value;
use std::collections::BTreeSet;
const GLB_VERSION: u32 = 2;
const GLB_JSON_CHUNK: u32 = 0x4e4f_534a;
const GLB_BIN_CHUNK: u32 = 0x004e_4942;
pub(crate) fn assemble(
manifest: &GltfCapabilityManifest,
root: &Value,
buffers: &[Vec<u8>],
modified: &BTreeSet<usize>,
) -> Result<Vec<u8>, GltfScaleRewriteError> {
let mut root = root.clone();
let binary_chunk = binary_chunk_buffer(manifest)?;
for &buffer_index in modified {
if Some(buffer_index) == binary_chunk {
continue;
}
let payload = buffers
.get(buffer_index)
.ok_or_else(|| unresolved_buffer(buffer_index))?;
reencode_data_uri(&mut root, buffer_index, payload)?;
}
let json = serde_json::to_vec(&root).map_err(WriteError::Serialize)?;
match manifest.container {
GltfContainerKind::Gltf => Ok(json),
GltfContainerKind::Glb => {
let bin = match binary_chunk {
Some(index) => buffers
.get(index)
.ok_or_else(|| unresolved_buffer(index))?
.clone(),
None => Vec::new(),
};
frame_glb(json, bin)
}
}
}
fn binary_chunk_buffer(
manifest: &GltfCapabilityManifest,
) -> Result<Option<usize>, GltfScaleRewriteError> {
let mut chunk_buffers = manifest
.buffers
.iter()
.filter(|buffer| buffer.source_kind == GltfBufferSourceKind::BinaryChunk)
.map(|buffer| buffer.buffer_index);
let first = chunk_buffers.next();
if chunk_buffers.next().is_some() {
return Err(GltfScaleRewriteError::UnreassemblableContainer {
reason: "more than one buffer is backed by the GLB BIN chunk",
});
}
if first.is_some_and(|index| index != 0) {
return Err(GltfScaleRewriteError::UnreassemblableContainer {
reason: "the GLB BIN chunk backs a buffer other than buffer 0",
});
}
Ok(first)
}
fn reencode_data_uri(
root: &mut Value,
buffer_index: usize,
payload: &[u8],
) -> Result<(), GltfScaleRewriteError> {
let location = format!("/buffers/{buffer_index}/uri");
let uri = root
.get("buffers")
.and_then(Value::as_array)
.and_then(|buffers| buffers.get(buffer_index))
.and_then(|buffer| buffer.get("uri"))
.and_then(Value::as_str)
.ok_or_else(|| {
LoadError::Buffer(format!(
"{location} is not a data URI that can be rewritten"
))
})?;
let prefix_len = uri
.find("base64,")
.map(|index| index + "base64,".len())
.ok_or_else(|| LoadError::Buffer(format!("{location} is not a base64 data URI")))?;
let rewritten = format!(
"{}{}",
&uri[..prefix_len],
base64::engine::general_purpose::STANDARD.encode(payload)
);
root["buffers"][buffer_index]["uri"] = Value::String(rewritten);
Ok(())
}
fn frame_glb(mut json: Vec<u8>, mut bin: Vec<u8>) -> Result<Vec<u8>, GltfScaleRewriteError> {
while !json.len().is_multiple_of(4) {
json.push(b' ');
}
while !bin.len().is_multiple_of(4) {
bin.push(0);
}
let lengths = plan_glb_lengths(json.len(), bin.len())?;
let mut out = Vec::with_capacity(lengths.total as usize);
out.extend_from_slice(b"glTF");
out.extend_from_slice(&GLB_VERSION.to_le_bytes());
out.extend_from_slice(&lengths.total.to_le_bytes());
out.extend_from_slice(&lengths.json.to_le_bytes());
out.extend_from_slice(&GLB_JSON_CHUNK.to_le_bytes());
out.extend_from_slice(&json);
if let Some(bin_len) = lengths.bin {
out.extend_from_slice(&bin_len.to_le_bytes());
out.extend_from_slice(&GLB_BIN_CHUNK.to_le_bytes());
out.extend_from_slice(&bin);
}
if glb_len_u32("total GLB length", out.len())? != lengths.total {
return Err(GltfScaleRewriteError::UnreassemblableContainer {
reason: "emitted GLB length disagrees with the planned header length",
});
}
Ok(out)
}
fn unresolved_buffer(buffer_index: usize) -> GltfScaleRewriteError {
LoadError::Buffer(format!("buffer {buffer_index} was not resolved")).into()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::capability::GltfBufferCapability;
use serde_json::json;
fn manifest(
container: GltfContainerKind,
kinds: &[GltfBufferSourceKind],
) -> GltfCapabilityManifest {
GltfCapabilityManifest {
container,
buffers: kinds
.iter()
.enumerate()
.map(|(buffer_index, &source_kind)| GltfBufferCapability {
buffer_index,
source_kind,
declared_byte_length: 4,
})
.collect(),
buffer_views: Vec::new(),
accessors: Vec::new(),
nodes: Vec::new(),
animation_channels: Vec::new(),
primitives: Vec::new(),
instancing: Vec::new(),
skins: Vec::new(),
camera_count: 0,
extensions: Vec::new(),
extension_locations: Vec::new(),
external_resource_locations: Vec::new(),
extras_locations: Vec::new(),
unknown_member_locations: Vec::new(),
}
}
#[test]
fn a_rewritten_data_uri_keeps_its_authored_prefix() {
let manifest = manifest(GltfContainerKind::Gltf, &[GltfBufferSourceKind::DataUri]);
let root = json!({
"buffers": [{ "uri": "data:model/gltf-buffer;charset=utf-8;base64,AAAAAA==", "byteLength": 4 }]
});
let out = assemble(
&manifest,
&root,
&[vec![1, 2, 3, 4]],
&BTreeSet::from([0usize]),
)
.expect("gltf reassembly");
let value: Value = serde_json::from_slice(&out).expect("valid JSON");
assert_eq!(
value["buffers"][0]["uri"],
json!("data:model/gltf-buffer;charset=utf-8;base64,AQIDBA==")
);
assert_eq!(value["buffers"][0]["byteLength"], json!(4));
}
#[test]
fn an_untouched_data_uri_is_copied_verbatim() {
let manifest = manifest(GltfContainerKind::Gltf, &[GltfBufferSourceKind::DataUri]);
let root = json!({
"buffers": [{ "uri": "data:application/octet-stream;base64,AAAAAA==", "byteLength": 4 }]
});
let out = assemble(&manifest, &root, &[vec![9, 9, 9, 9]], &BTreeSet::new())
.expect("gltf reassembly");
let value: Value = serde_json::from_slice(&out).expect("valid JSON");
assert_eq!(
value["buffers"][0]["uri"],
json!("data:application/octet-stream;base64,AAAAAA==")
);
}
#[test]
fn glb_framing_pads_both_chunks_and_agrees_with_its_own_header() {
let manifest = manifest(GltfContainerKind::Glb, &[GltfBufferSourceKind::BinaryChunk]);
let root = json!({ "buffers": [{ "byteLength": 5 }] });
let out = assemble(
&manifest,
&root,
&[vec![1, 2, 3, 4, 5]],
&BTreeSet::from([0usize]),
)
.expect("glb reassembly");
assert_eq!(&out[0..4], b"glTF");
assert_eq!(u32::from_le_bytes(out[4..8].try_into().unwrap()), 2);
assert_eq!(
u32::from_le_bytes(out[8..12].try_into().unwrap()) as usize,
out.len()
);
let json_len = u32::from_le_bytes(out[12..16].try_into().unwrap()) as usize;
assert!(json_len.is_multiple_of(4));
assert_eq!(
u32::from_le_bytes(out[16..20].try_into().unwrap()),
GLB_JSON_CHUNK
);
assert_eq!(&out[20 + json_len - 1..20 + json_len], b" ");
let bin_start = 20 + json_len;
let bin_len =
u32::from_le_bytes(out[bin_start..bin_start + 4].try_into().unwrap()) as usize;
assert_eq!(bin_len, 8, "5 payload bytes pad to 8");
assert_eq!(
u32::from_le_bytes(out[bin_start + 4..bin_start + 8].try_into().unwrap()),
GLB_BIN_CHUNK
);
assert_eq!(
&out[bin_start + 8..bin_start + 8 + bin_len],
&[1, 2, 3, 4, 5, 0, 0, 0]
);
assert_eq!(out.len(), 12 + 8 + json_len + 8 + bin_len);
}
#[test]
fn a_glb_binary_chunk_outside_buffer_zero_is_refused() {
let manifest = manifest(
GltfContainerKind::Glb,
&[
GltfBufferSourceKind::DataUri,
GltfBufferSourceKind::BinaryChunk,
],
);
let root = json!({ "buffers": [{ "uri": "data:x;base64,AAAAAA==" }, { "byteLength": 4 }] });
let error = assemble(
&manifest,
&root,
&[vec![0; 4], vec![0; 4]],
&BTreeSet::new(),
)
.expect_err("BIN chunk must back buffer 0");
assert!(matches!(
error,
GltfScaleRewriteError::UnreassemblableContainer {
reason: "the GLB BIN chunk backs a buffer other than buffer 0"
}
));
}
#[test]
fn two_binary_chunk_buffers_are_refused() {
let manifest = manifest(
GltfContainerKind::Glb,
&[
GltfBufferSourceKind::BinaryChunk,
GltfBufferSourceKind::BinaryChunk,
],
);
let root = json!({ "buffers": [{ "byteLength": 4 }, { "byteLength": 4 }] });
let error = assemble(
&manifest,
&root,
&[vec![0; 4], vec![0; 4]],
&BTreeSet::new(),
)
.expect_err("two BIN-backed buffers are ambiguous");
assert!(matches!(
error,
GltfScaleRewriteError::UnreassemblableContainer {
reason: "more than one buffer is backed by the GLB BIN chunk"
}
));
}
}