1use crate::binary::{Error, Reader, Result, Writer};
8use serde_json::{Map, Value, json};
9use std::sync::OnceLock;
10
11pub(crate) const RECORD_FORMATS: &[&str] = &[
13 "n3pmesh", "n3cskin", "n3cskins", "n3cpart", "n3cplug", "n3shape", "n3chr", "n3joint",
14 "n3anim", "n3fxplug", "n3sky", "fxb", "gtd", "opd", "uif",
15];
16
17pub type Record = Map<String, Value>;
19
20#[derive(Clone, Debug)]
22pub struct Limits {
23 pub bytes: usize,
25 pub nodes: usize,
27 pub depth: usize,
29 pub parameters: std::collections::BTreeMap<String, i64>,
31}
32impl Default for Limits {
33 fn default() -> Self {
34 Self {
35 bytes: 256 * 1024 * 1024,
36 nodes: 8_000_000,
37 depth: 128,
38 parameters: Default::default(),
39 }
40 }
41}
42
43fn error(message: impl Into<String>) -> Error {
44 Error {
45 offset: 0,
46 message: message.into(),
47 }
48}
49fn definitions() -> &'static Value {
50 static SCHEMAS: OnceLock<Value> = OnceLock::new();
51 SCHEMAS.get_or_init(|| {
52 serde_json::from_str(include_str!("../layouts.json")).expect("Valid bundled format layouts")
53 })
54}
55
56pub fn hex(bytes: &[u8]) -> String {
58 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
59}
60pub fn unhex(value: &str) -> Result<Vec<u8>> {
62 if !value.len().is_multiple_of(2) || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
63 return Err(error("Invalid hex byte string"));
64 }
65 value
66 .as_bytes()
67 .as_chunks::<2>()
68 .0
69 .iter()
70 .map(|pair| {
71 let digit = |byte: u8| match byte {
72 b'0'..=b'9' => byte - b'0',
73 b'a'..=b'f' => byte - b'a' + 10,
74 _ => byte - b'A' + 10,
75 };
76 Ok(digit(pair[0]) * 16 + digit(pair[1]))
77 })
78 .collect()
79}
80pub fn utf8(value: &str) -> String {
82 hex(value.as_bytes())
83}
84
85fn expr(value: &Value, context: &Value) -> Result<i64> {
86 if let Some(number) = value.as_i64() {
87 return Ok(number);
88 }
89 if let Some(name) = value.as_str() {
90 return context[name]
91 .as_i64()
92 .ok_or_else(|| error(format!("Missing numeric field {name}")));
93 }
94 if let Some(parts) = value["product"].as_array() {
95 return parts.iter().try_fold(1_i64, |product, part| {
96 product
97 .checked_mul(expr(part, context)?)
98 .ok_or_else(|| error("Size overflow"))
99 });
100 }
101 if let Some(parts) = value["gt"].as_array() {
102 return Ok(i64::from(
103 expr(&parts[0], context)? > expr(&parts[1], context)?,
104 ));
105 }
106 if let Some(parts) = value["eq"].as_array() {
107 return Ok(i64::from(
108 expr(&parts[0], context)? == expr(&parts[1], context)?,
109 ));
110 }
111 if let Some(parts) = value["minus"].as_array() {
112 return expr(&parts[0], context)?
113 .checked_sub(expr(&parts[1], context)?)
114 .ok_or_else(|| error("Expression overflow"));
115 }
116 if let Some(parts) = value["divide"].as_array() {
117 let divisor = expr(&parts[1], context)?;
118 if divisor <= 0 {
119 return Err(error("Invalid layout divisor"));
120 }
121 return Ok(expr(&parts[0], context)?.div_euclid(divisor));
122 }
123 if let Some(parts) = value["mask"].as_array() {
124 return Ok(expr(&parts[0], context)? & expr(&parts[1], context)?);
125 }
126 if let Some(parts) = value["and"].as_array() {
127 for part in parts {
128 if expr(part, context)? == 0 {
129 return Ok(0);
130 }
131 }
132 return Ok(1);
133 }
134 if let Some(parts) = value["or"].as_array() {
135 for part in parts {
136 if expr(part, context)? != 0 {
137 return Ok(1);
138 }
139 }
140 return Ok(0);
141 }
142 if !value["float"].is_null() {
143 let number = f32::from_bits(
144 u32::try_from(expr(&value["float"], context)?)
145 .map_err(|_| error("Invalid f32 bits"))?,
146 );
147 if !number.is_finite() {
148 return Err(error("Nonfinite layout dimension"));
149 }
150 return Ok(number.floor() as i64);
151 }
152 if let Some(name) = value["length"].as_str() {
153 return context[name]
154 .as_str()
155 .map(|text| text.len() as i64 / 2)
156 .ok_or_else(|| error("Expected hex string"));
157 }
158 if let Some(parts) = value["choose"].as_array() {
159 return expr(
160 &parts[if expr(&parts[0], context)? != 0 { 1 } else { 2 }],
161 context,
162 );
163 }
164 if let Some(parts) = value["select"].as_array() {
165 let tag = expr(&parts[0], context)?;
166 if !(0..=1).contains(&tag) {
167 return Err(error("Unsupported animation key type"));
168 }
169 return expr(&parts[tag as usize + 1], context);
170 }
171 Err(error("Invalid layout expression"))
172}
173fn count(value: &Value, context: &Value) -> Result<usize> {
174 let value = expr(value, context)?;
175 if !(0..=8_000_000).contains(&value) {
176 return Err(error("Invalid array count"));
177 }
178 Ok(value as usize)
179}
180fn minimum(schema: &Value) -> usize {
181 if let Some(name) = schema.as_str() {
182 if name == "ntf" {
183 return 24;
184 }
185 if !definitions()[name].is_null() {
186 return minimum(&definitions()[name]);
187 }
188 return match name {
189 "string" | "f32" | "u32" | "i32" => 4,
190 "u16" | "i16" => 2,
191 _ => 1,
192 };
193 }
194 if schema["kind"] == "struct" {
195 return schema["fields"].as_array().map_or(0, |fields| {
196 fields
197 .iter()
198 .filter(|field| field["when"].is_null() && field["optional"] != true)
199 .map(|field| minimum(&field["type"]))
200 .sum()
201 });
202 }
203 if schema["kind"] == "choice" {
204 return minimum(&schema["yes"]).min(minimum(&schema["no"]));
205 }
206 schema["count"].as_u64().map_or(0, |count| {
207 count as usize
208 * if schema["kind"] == "bytes" {
209 1
210 } else {
211 minimum(&schema["item"])
212 }
213 })
214}
215enum Io<'a> {
216 Read(Reader<'a>),
217 Write(Writer),
218}
219struct State<'a> {
220 io: Io<'a>,
221 nodes: usize,
222 depth: usize,
223 parameters: std::collections::BTreeMap<String, i64>,
224}
225fn scope(parent: &Value, local: &Value) -> Value {
226 let mut scope = Map::new();
227 for value in [parent, local] {
228 if let Some(fields) = value.as_object() {
229 for (name, value) in fields {
230 if value.is_number() || value.is_string() {
231 scope.insert(name.clone(), value.clone());
232 }
233 }
234 }
235 }
236 Value::Object(scope)
237}
238impl State<'_> {
239 fn reading(&self) -> bool {
240 matches!(self.io, Io::Read(_))
241 }
242 fn traverse(
243 &mut self,
244 schema: &Value,
245 context: &Value,
246 input: &Value,
247 depth: usize,
248 ) -> Result<Value> {
249 if self.nodes == 0 || depth > self.depth {
250 return Err(error("Record complexity limit exceeded"));
251 }
252 self.nodes -= 1;
253 if let Some(name) = schema.as_str() {
254 if name == "ntf" {
255 return match &mut self.io {
256 Io::Read(reader) => {
257 serde_json::to_value(crate::textures::read_texture(reader)?)
258 .map_err(|_| error("Texture serialization failed"))
259 }
260 Io::Write(writer) => {
261 let texture = serde_json::from_value(input.clone())
262 .map_err(|_| error("Invalid texture record"))?;
263 crate::textures::append_texture(writer, &texture)?;
264 Ok(input.clone())
265 }
266 };
267 }
268 if !definitions()[name].is_null() {
269 return self.traverse(&definitions()[name], context, input, depth + 1);
270 }
271 return match &mut self.io {
272 Io::Read(reader) => Ok(match name {
273 "string" => json!(hex(&reader.string(1_048_576)?)),
274 "u8" => json!(reader.u8()?),
275 "i8" => json!(reader.i8()?),
276 "u16" => json!(reader.u16()?),
277 "i16" => json!(reader.i16()?),
278 "u32" | "f32" => json!(reader.u32()?),
279 "i32" => json!(reader.i32()?),
280 _ => return Err(error("Unknown primitive")),
281 }),
282 Io::Write(writer) => {
283 if name == "string" {
284 writer.string(&unhex(
285 input.as_str().ok_or_else(|| error("Expected hex string"))?,
286 )?)?;
287 } else {
288 let value = input
289 .as_i64()
290 .ok_or_else(|| error("Expected integer wire value"))?;
291 let invalid = |_| error(format!("Value outside {name} range"));
292 match name {
293 "u8" => writer.u8(u8::try_from(value).map_err(invalid)?)?,
294 "i8" => writer.i8(i8::try_from(value).map_err(invalid)?)?,
295 "u16" => writer.u16(u16::try_from(value).map_err(invalid)?)?,
296 "i16" => writer.i16(i16::try_from(value).map_err(invalid)?)?,
297 "u32" | "f32" => writer.u32(u32::try_from(value).map_err(invalid)?)?,
298 "i32" => writer.i32(i32::try_from(value).map_err(invalid)?)?,
299 _ => return Err(error("Unknown primitive")),
300 }
301 }
302 Ok(input.clone())
303 }
304 };
305 }
306 if schema["kind"] == "choice" {
307 return self.traverse(
308 &schema[if expr(&schema["condition"], context)? != 0 {
309 "yes"
310 } else {
311 "no"
312 }],
313 context,
314 input,
315 depth + 1,
316 );
317 }
318 if schema["kind"] == "parameter" {
319 let name = schema["name"].as_str().unwrap();
320 let value = if self.reading() {
321 self.parameters
322 .get(name)
323 .copied()
324 .unwrap_or(schema["default"].as_i64().unwrap())
325 } else {
326 input
327 .as_i64()
328 .ok_or_else(|| error("Missing layout parameter"))?
329 };
330 if !(0..=65536).contains(&value) {
331 return Err(error("Invalid layout parameter"));
332 }
333 return Ok(json!(value));
334 }
335 if schema["kind"] == "tail" {
336 return match &mut self.io {
337 Io::Read(reader) => Ok(json!(hex(reader.raw(reader.remaining())?))),
338 Io::Write(writer) => {
339 writer.raw(&unhex(
340 input
341 .as_str()
342 .ok_or_else(|| error("Expected tail hex bytes"))?,
343 )?)?;
344 Ok(input.clone())
345 }
346 };
347 }
348 if schema["kind"] == "struct" {
349 let fields = schema["fields"]
350 .as_array()
351 .ok_or_else(|| error("Missing layout fields"))?;
352 let mut result = if self.reading() {
353 json!({})
354 } else {
355 if !input.is_object() {
356 return Err(error("Expected record"));
357 }
358 input.clone()
359 };
360 if !self.reading() {
361 for field in fields {
362 if let Some(array) = field["sizeOf"].as_str() {
363 if !field["when"].is_null()
364 && expr(&field["when"], &scope(context, &result))? == 0
365 {
366 continue;
367 }
368 if result.get(array).is_none()
369 && (field["optional"] == true
370 || result[field["name"].as_str().unwrap()] == 0)
371 {
372 continue;
373 }
374 let length = result[array]
375 .as_array()
376 .ok_or_else(|| error(format!("Expected array {array}")))?
377 .len();
378 result[field["name"].as_str().unwrap()] = json!(length);
379 }
380 }
381 }
382 let mut omitted = false;
383 for field in fields {
384 let name = field["name"].as_str().unwrap();
385 if !field["when"].is_null() && expr(&field["when"], &scope(context, &result))? == 0
386 {
387 continue;
388 }
389 let absent = match &self.io {
390 Io::Read(reader) => reader.remaining() == 0,
391 Io::Write(_) => result.get(name).is_none(),
392 };
393 if field["optional"] == true && absent {
394 omitted = true;
395 continue;
396 }
397 if omitted {
398 return Err(error("Optional tail fields must form a contiguous prefix"));
399 }
400 result[name] = self.traverse(
401 &field["type"],
402 &scope(context, &result),
403 &result[name],
404 depth + 1,
405 )?;
406 }
407 return Ok(result);
408 }
409 let length = count(&schema["count"], context)?;
410 if schema["kind"] == "bytes" {
411 return match &mut self.io {
412 Io::Read(reader) => Ok(json!(hex(reader.raw(length)?))),
413 Io::Write(writer) => {
414 let bytes = unhex(
415 input
416 .as_str()
417 .ok_or_else(|| error("Expected opaque hex bytes"))?,
418 )?;
419 if bytes.len() != length {
420 return Err(error("Opaque byte count mismatch"));
421 }
422 writer.raw(&bytes)?;
423 Ok(input.clone())
424 }
425 };
426 }
427 if length > self.nodes {
428 return Err(error("Array exceeds node budget"));
429 }
430 if let Io::Read(reader) = &self.io
431 && length.saturating_mul(minimum(&schema["item"]).max(1)) > reader.remaining()
432 {
433 return Err(error("Array exceeds available bytes"));
434 }
435 if !self.reading() && input.as_array().is_none_or(|array| array.len() != length) {
436 return Err(error("Array count mismatch"));
437 }
438 let mut values = Vec::new();
439 values
440 .try_reserve(length)
441 .map_err(|_| error("Array allocation failed"))?;
442 for index in 0..length {
443 values.push(self.traverse(&schema["item"], context, &input[index], depth + 1)?);
444 }
445 Ok(Value::Array(values))
446 }
447}
448
449pub(crate) fn parse_record(format: &str, bytes: &[u8]) -> Result<Record> {
451 parse_record_with_limits(format, bytes, Limits::default())
452}
453pub(crate) fn parse_record_with_limits(
455 format: &str,
456 bytes: &[u8],
457 limits: Limits,
458) -> Result<Record> {
459 if !RECORD_FORMATS.contains(&format) {
460 return Err(error(format!("Unknown record format {format}")));
461 }
462 if bytes.len() > limits.bytes {
463 return Err(error("Input byte budget exceeded"));
464 }
465 let mut state = State {
466 io: Io::Read(Reader::new(bytes)),
467 nodes: limits.nodes,
468 depth: limits.depth,
469 parameters: limits.parameters,
470 };
471 let record = state.traverse(&json!(format), &json!({}), &Value::Null, 0)?;
472 if let Io::Read(reader) = state.io {
473 reader.end()?;
474 }
475 validate(format, &record)?;
476 Ok(record.as_object().unwrap().clone())
477}
478pub(crate) fn write_record(format: &str, record: &Record) -> Result<Vec<u8>> {
480 write_record_with_limits(format, record, Limits::default())
481}
482pub(crate) fn write_record_with_limits(
484 format: &str,
485 record: &Record,
486 mut limits: Limits,
487) -> Result<Vec<u8>> {
488 if !RECORD_FORMATS.contains(&format) {
489 return Err(error("Unknown record format"));
490 }
491 let mut state = State {
492 io: Io::Write(Writer::new(limits.bytes)),
493 nodes: limits.nodes,
494 depth: limits.depth,
495 parameters: limits.parameters.clone(),
496 };
497 state.traverse(
498 &json!(format),
499 &json!({}),
500 &Value::Object(record.clone()),
501 0,
502 )?;
503 let Io::Write(writer) = state.io else {
504 unreachable!()
505 };
506 let bytes = writer.finish();
507 for name in ["gridCell", "fileVersion", "spacingPresent", "type"] {
508 if let Some(value) = record.get(name).and_then(Value::as_i64) {
509 limits.parameters.insert(name.into(), value);
510 }
511 }
512 parse_record_with_limits(format, &bytes, limits)?;
513 Ok(bytes)
514}
515fn validate(format: &str, record: &Value) -> Result<()> {
516 if format == "fxb" {
517 if !matches!(record["version"].as_i64(), Some(0..=3)) {
518 return Err(error("Unsupported FXB version"));
519 }
520 for slot in record["slots"].as_array().unwrap() {
521 if !matches!(slot["type"].as_i64(), Some(0..=4)) {
522 return Err(error("Unsupported FXB part type"));
523 }
524 if slot["type"] != 0
525 && (!matches!(slot["version"].as_i64(), Some(0..=7))
526 || !matches!(slot["baseVersion"].as_i64(), Some(0..=2)))
527 {
528 return Err(error("Unsupported FXB part version"));
529 }
530 }
531 if record["version"] == 3
532 && !matches!(record["trailer"].as_str().unwrap_or("").len(), 88 | 90)
533 {
534 return Err(error("FXB v3 requires its observed 44/45-byte trailer"));
535 }
536 }
537 if format == "gtd"
538 && (!matches!(record["version"].as_i64(), Some(0..=2))
539 || !matches!(record["mapSize"].as_i64(), Some(1..=4097)))
540 {
541 return Err(error("Invalid GTD version or dimensions"));
542 }
543 if format == "opd" && !matches!(record["gridCell"].as_i64(), Some(1..=1024)) {
544 return Err(error("Invalid OPD grid cell"));
545 }
546 if format == "n3pmesh" {
547 let vertices = record["vertexCount"].as_i64().unwrap_or(-1);
548 let indices = record["indices"].as_array().unwrap();
549 if !(1..=2_000_000).contains(&vertices)
550 || indices.is_empty()
551 || !indices.len().is_multiple_of(3)
552 || indices
553 .iter()
554 .any(|index| index.as_i64().unwrap_or(-1) >= vertices)
555 {
556 return Err(error("Invalid mesh vertices or indices"));
557 }
558 if !(0..=vertices).contains(&record["minVertexCount"].as_i64().unwrap_or(-1))
559 || !(0..=indices.len() as i64).contains(&record["minIndexCount"].as_i64().unwrap_or(-1))
560 {
561 return Err(error("Invalid minimum mesh counts"));
562 }
563 }
564 if format == "n3cskin" {
565 let vertices = record["vertexCount"].as_i64().unwrap_or(-1);
566 let uvs = record["uvCount"].as_i64().unwrap_or(-1);
567 for (field, maximum) in [("vertexIndices", vertices), ("uvIndices", uvs)] {
568 if record[field]
569 .as_array()
570 .unwrap()
571 .iter()
572 .any(|index| index.as_i64().unwrap_or(-1) >= maximum)
573 {
574 return Err(error("Skin index out of range"));
575 }
576 }
577 let faces = record["faceCount"].as_i64().unwrap_or(-1);
578 if (vertices == 0 || faces == 0) && (vertices != 0 || faces != 0 || uvs != 0) {
579 return Err(error("Inconsistent empty skin"));
580 }
581 }
582 if format == "n3cskins" {
583 for lod in record["lods"].as_array().unwrap() {
584 validate("n3cskin", lod)?;
585 }
586 }
587 Ok(())
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593 #[test]
594 fn sky_roundtrip_and_edit() -> Result<()> {
595 let value = json!({"suns":["00ff","", ""],"clouds":["","","","","",""],"moon":"", "dayChanges":[{"name":"202000", "type":0,"when":500,"param1":4294967295_u32,"param2":0,"howLong":2141266757_u32}]});
596 let bytes = write_record("n3sky", value.as_object().unwrap())?;
597 let mut record = parse_record("n3sky", &bytes)?;
598 assert_eq!(write_record("n3sky", &record)?, bytes);
599 record.insert("moon".into(), json!(utf8("new moon")));
600 assert_eq!(
601 parse_record("n3sky", &write_record("n3sky", &record)?)?["moon"],
602 utf8("new moon")
603 );
604 assert!(parse_record("n3sky", &bytes[..bytes.len() - 1]).is_err());
605 Ok(())
606 }
607}