1use std::collections::HashMap;
26
27use anyhow::{bail, Context, Result};
28use serde::{Deserialize, Serialize};
29
30use crate::pack_distribute::{PackBuilder, PackVerifier};
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub enum TextureFormat {
37 Png,
38 Jpeg,
39 Exr,
40}
41
42impl TextureFormat {
43 fn extension(&self) -> &'static str {
44 match self {
45 TextureFormat::Png => "png",
46 TextureFormat::Jpeg => "jpg",
47 TextureFormat::Exr => "exr",
48 }
49 }
50
51 #[allow(dead_code)]
52 fn mime_type(&self) -> &'static str {
53 match self {
54 TextureFormat::Png => "image/png",
55 TextureFormat::Jpeg => "image/jpeg",
56 TextureFormat::Exr => "image/x-exr",
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct TextureAsset {
66 pub name: String,
68 pub width: u32,
70 pub height: u32,
72 pub channels: u8,
74 pub data: Vec<u8>,
76 pub format: TextureFormat,
78}
79
80impl TextureAsset {
81 pub fn validate(&self) -> Result<()> {
83 if self.name.is_empty() {
84 bail!("texture name must not be empty");
85 }
86 if self.width == 0 || self.height == 0 {
87 bail!("texture dimensions must be non-zero");
88 }
89 if self.channels == 0 || self.channels > 4 {
90 bail!("texture channels must be 1–4, got {}", self.channels);
91 }
92 let expected = self.width as usize * self.height as usize * self.channels as usize;
93 if self.data.len() != expected {
94 bail!(
95 "texture '{}': expected {} bytes ({} x {} x {}), got {}",
96 self.name,
97 expected,
98 self.width,
99 self.height,
100 self.channels,
101 self.data.len()
102 );
103 }
104 Ok(())
105 }
106
107 fn pack_path(&self) -> String {
109 format!("textures/{}.{}", self.name, self.format.extension())
110 }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct MaterialDef {
118 pub name: String,
120 pub albedo_color: [f32; 4],
122 pub metallic: f32,
124 pub roughness: f32,
126 pub emissive: [f32; 3],
128 pub albedo_texture: Option<String>,
130 pub normal_texture: Option<String>,
132}
133
134impl MaterialDef {
135 pub fn validate(&self) -> Result<()> {
137 if self.name.is_empty() {
138 bail!("material name must not be empty");
139 }
140 if !(0.0..=1.0).contains(&self.metallic) {
141 bail!(
142 "material '{}': metallic {} is out of [0,1]",
143 self.name,
144 self.metallic
145 );
146 }
147 if !(0.0..=1.0).contains(&self.roughness) {
148 bail!(
149 "material '{}': roughness {} is out of [0,1]",
150 self.name,
151 self.roughness
152 );
153 }
154 Ok(())
155 }
156
157 fn pack_path(&self) -> String {
159 format!("materials/{}.json", self.name)
160 }
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct MorphPreset {
168 pub name: String,
170 pub description: String,
172 pub params: HashMap<String, f64>,
174 pub tags: Vec<String>,
176}
177
178impl MorphPreset {
179 pub fn validate(&self) -> Result<()> {
181 if self.name.is_empty() {
182 bail!("morph preset name must not be empty");
183 }
184 Ok(())
185 }
186
187 fn pack_path(&self) -> String {
189 let slug: String = self
190 .name
191 .to_lowercase()
192 .chars()
193 .map(|c| if c.is_alphanumeric() { c } else { '_' })
194 .collect();
195 format!("presets/{}.json", slug)
196 }
197}
198
199#[derive(Debug, Clone)]
203pub enum AssetPackEntry {
204 Target(TargetDelta),
206 Texture(TextureAsset),
208 Material(MaterialDef),
210 Preset(MorphPreset),
212}
213
214#[derive(Debug, Clone)]
216pub struct TargetDelta {
217 pub name: String,
219 pub data: Vec<u8>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct AssetPackMeta {
228 pub version: String,
230 pub author: String,
232 pub license: String,
234 pub description: String,
236 pub created_at: u64,
238}
239
240impl Default for AssetPackMeta {
241 fn default() -> Self {
242 Self {
243 version: "0.1.0".to_string(),
244 author: String::new(),
245 license: "Apache-2.0".to_string(),
246 description: String::new(),
247 created_at: 0,
248 }
249 }
250}
251
252pub struct AssetPackBuilder {
274 name: String,
275 meta: AssetPackMeta,
276 entries: Vec<AssetPackEntry>,
277}
278
279impl AssetPackBuilder {
280 pub fn new(name: &str) -> Self {
282 Self {
283 name: name.to_string(),
284 meta: AssetPackMeta::default(),
285 entries: Vec::new(),
286 }
287 }
288
289 pub fn set_meta(&mut self, meta: AssetPackMeta) -> &mut Self {
291 self.meta = meta;
292 self
293 }
294
295 pub fn set_author(&mut self, author: &str) -> &mut Self {
297 self.meta.author = author.to_string();
298 self
299 }
300
301 pub fn set_version(&mut self, version: &str) -> &mut Self {
303 self.meta.version = version.to_string();
304 self
305 }
306
307 pub fn set_license(&mut self, license: &str) -> &mut Self {
309 self.meta.license = license.to_string();
310 self
311 }
312
313 pub fn set_description(&mut self, desc: &str) -> &mut Self {
315 self.meta.description = desc.to_string();
316 self
317 }
318
319 pub fn add_target(&mut self, delta: TargetDelta) -> &mut Self {
321 self.entries.push(AssetPackEntry::Target(delta));
322 self
323 }
324
325 pub fn add_texture(&mut self, tex: TextureAsset) -> Result<&mut Self> {
327 tex.validate()?;
328 self.entries.push(AssetPackEntry::Texture(tex));
329 Ok(self)
330 }
331
332 pub fn add_material(&mut self, mat: MaterialDef) -> Result<&mut Self> {
334 mat.validate()?;
335 self.entries.push(AssetPackEntry::Material(mat));
336 Ok(self)
337 }
338
339 pub fn add_preset(&mut self, preset: MorphPreset) -> Result<&mut Self> {
341 preset.validate()?;
342 self.entries.push(AssetPackEntry::Preset(preset));
343 Ok(self)
344 }
345
346 fn add_preset_unchecked(&mut self, preset: MorphPreset) -> &mut Self {
348 self.entries.push(AssetPackEntry::Preset(preset));
349 self
350 }
351
352 fn add_material_unchecked(&mut self, mat: MaterialDef) -> &mut Self {
354 self.entries.push(AssetPackEntry::Material(mat));
355 self
356 }
357
358 pub fn build(&self) -> Result<Vec<u8>> {
360 let mut pack = PackBuilder::new(&self.name, &self.meta.version, &self.meta.author);
361 pack.set_description(&self.meta.description);
362 pack.set_license(&self.meta.license);
363 pack.set_created_at(self.meta.created_at);
364
365 for entry in &self.entries {
366 match entry {
367 AssetPackEntry::Target(delta) => {
368 pack.add_target_file(&delta.name, "targets", &delta.data)
369 .with_context(|| format!("failed to add target '{}'", delta.name))?;
370 }
371 AssetPackEntry::Texture(tex) => {
372 let json = serde_json::to_vec(tex)
373 .with_context(|| format!("failed to serialize texture '{}'", tex.name))?;
374 pack.add_target_file(&tex.pack_path(), "textures", &json)
375 .with_context(|| format!("failed to add texture '{}'", tex.name))?;
376 }
377 AssetPackEntry::Material(mat) => {
378 let json = serde_json::to_vec(mat)
379 .with_context(|| format!("failed to serialize material '{}'", mat.name))?;
380 pack.add_target_file(&mat.pack_path(), "materials", &json)
381 .with_context(|| format!("failed to add material '{}'", mat.name))?;
382 }
383 AssetPackEntry::Preset(preset) => {
384 let json = serde_json::to_vec(preset)
385 .with_context(|| format!("failed to serialize preset '{}'", preset.name))?;
386 pack.add_target_file(&preset.pack_path(), "presets", &json)
387 .with_context(|| format!("failed to add preset '{}'", preset.name))?;
388 }
389 }
390 }
391
392 pack.build()
393 }
394}
395
396#[derive(Debug, Clone)]
402pub struct AssetPackIndex {
403 pub name: String,
405 pub version: String,
407 pub author: String,
409 pub license: String,
411 pub description: String,
413 pub textures: Vec<TextureAsset>,
415 pub materials: Vec<MaterialDef>,
417 pub presets: Vec<MorphPreset>,
419 pub target_names: Vec<String>,
421 pub total_bytes: usize,
423}
424
425pub fn load_pack_from_bytes(bytes: &[u8]) -> Result<AssetPackIndex> {
431 let manifest =
433 PackVerifier::verify_integrity(bytes).with_context(|| "OXP integrity check failed")?;
434
435 let mut index = AssetPackIndex {
436 name: manifest.name.clone(),
437 version: manifest.version.clone(),
438 author: manifest.author.clone(),
439 license: manifest.license.clone(),
440 description: manifest.description.clone(),
441 textures: Vec::new(),
442 materials: Vec::new(),
443 presets: Vec::new(),
444 target_names: Vec::new(),
445 total_bytes: bytes.len(),
446 };
447
448 for entry in &manifest.targets {
450 match entry.category.as_str() {
451 "textures" => {
452 let data = PackVerifier::extract_file(bytes, &entry.file_path)
453 .with_context(|| format!("extracting texture '{}'", entry.file_path))?;
454 let tex: TextureAsset = serde_json::from_slice(&data)
455 .with_context(|| format!("deserializing texture '{}'", entry.file_path))?;
456 index.textures.push(tex);
457 }
458 "materials" => {
459 let data = PackVerifier::extract_file(bytes, &entry.file_path)
460 .with_context(|| format!("extracting material '{}'", entry.file_path))?;
461 let mat: MaterialDef = serde_json::from_slice(&data)
462 .with_context(|| format!("deserializing material '{}'", entry.file_path))?;
463 index.materials.push(mat);
464 }
465 "presets" => {
466 let data = PackVerifier::extract_file(bytes, &entry.file_path)
467 .with_context(|| format!("extracting preset '{}'", entry.file_path))?;
468 let preset: MorphPreset = serde_json::from_slice(&data)
469 .with_context(|| format!("deserializing preset '{}'", entry.file_path))?;
470 index.presets.push(preset);
471 }
472 "targets" => {
473 index.target_names.push(entry.name.clone());
474 }
475 other => {
476 let _ = other;
478 }
479 }
480 }
481
482 Ok(index)
483}
484
485pub fn build_alpha_pack() -> Vec<u8> {
496 build_alpha_pack_inner().unwrap_or_else(|_| Vec::new())
497}
498
499fn build_alpha_pack_inner() -> Result<Vec<u8>> {
500 let mut builder = AssetPackBuilder::new("oxihuman-alpha");
501 builder
502 .set_version("0.1.0-alpha")
503 .set_author("COOLJAPAN OU (Team Kitasan)")
504 .set_license("Apache-2.0")
505 .set_description("OxiHuman alpha sample asset pack — body presets and PBR materials.");
506
507 builder.add_preset_unchecked(MorphPreset {
510 name: "Athletic".to_string(),
511 description: "Well-developed musculature, low body fat, balanced proportions.".to_string(),
512 params: {
513 let mut m = HashMap::new();
514 m.insert("muscle_mass".to_string(), 0.75);
515 m.insert("body_fat".to_string(), 0.12);
516 m.insert("height_scale".to_string(), 1.0);
517 m.insert("shoulder_width".to_string(), 0.6);
518 m.insert("waist_width".to_string(), 0.38);
519 m
520 },
521 tags: vec![
522 "fitness".to_string(),
523 "sport".to_string(),
524 "body".to_string(),
525 ],
526 });
527
528 builder.add_preset_unchecked(MorphPreset {
529 name: "Slim".to_string(),
530 description: "Slender frame with minimal muscle definition and low body fat.".to_string(),
531 params: {
532 let mut m = HashMap::new();
533 m.insert("muscle_mass".to_string(), 0.30);
534 m.insert("body_fat".to_string(), 0.10);
535 m.insert("height_scale".to_string(), 1.0);
536 m.insert("shoulder_width".to_string(), 0.42);
537 m.insert("waist_width".to_string(), 0.32);
538 m
539 },
540 tags: vec!["slim".to_string(), "body".to_string()],
541 });
542
543 builder.add_preset_unchecked(MorphPreset {
544 name: "Heavy".to_string(),
545 description: "Larger frame with higher body fat and increased mass.".to_string(),
546 params: {
547 let mut m = HashMap::new();
548 m.insert("muscle_mass".to_string(), 0.45);
549 m.insert("body_fat".to_string(), 0.38);
550 m.insert("height_scale".to_string(), 1.0);
551 m.insert("shoulder_width".to_string(), 0.68);
552 m.insert("waist_width".to_string(), 0.62);
553 m
554 },
555 tags: vec![
556 "heavy".to_string(),
557 "overweight".to_string(),
558 "body".to_string(),
559 ],
560 });
561
562 builder.add_preset_unchecked(MorphPreset {
563 name: "Tall".to_string(),
564 description: "Above-average height with proportionally elongated limbs.".to_string(),
565 params: {
566 let mut m = HashMap::new();
567 m.insert("muscle_mass".to_string(), 0.50);
568 m.insert("body_fat".to_string(), 0.18);
569 m.insert("height_scale".to_string(), 1.20);
570 m.insert("leg_length".to_string(), 0.65);
571 m.insert("torso_length".to_string(), 0.60);
572 m
573 },
574 tags: vec!["tall".to_string(), "height".to_string(), "body".to_string()],
575 });
576
577 builder.add_preset_unchecked(MorphPreset {
578 name: "Short".to_string(),
579 description: "Below-average height with proportionally compact build.".to_string(),
580 params: {
581 let mut m = HashMap::new();
582 m.insert("muscle_mass".to_string(), 0.50);
583 m.insert("body_fat".to_string(), 0.18);
584 m.insert("height_scale".to_string(), 0.82);
585 m.insert("leg_length".to_string(), 0.45);
586 m.insert("torso_length".to_string(), 0.44);
587 m
588 },
589 tags: vec![
590 "short".to_string(),
591 "height".to_string(),
592 "body".to_string(),
593 ],
594 });
595
596 builder.add_material_unchecked(MaterialDef {
599 name: "Skin".to_string(),
600 albedo_color: [0.87, 0.72, 0.60, 1.0],
601 metallic: 0.0,
602 roughness: 0.70,
603 emissive: [0.0, 0.0, 0.0],
604 albedo_texture: Some("skin_albedo".to_string()),
605 normal_texture: Some("skin_normal".to_string()),
606 });
607
608 builder.add_material_unchecked(MaterialDef {
609 name: "Cloth".to_string(),
610 albedo_color: [0.40, 0.40, 0.55, 1.0],
611 metallic: 0.0,
612 roughness: 0.90,
613 emissive: [0.0, 0.0, 0.0],
614 albedo_texture: Some("cloth_albedo".to_string()),
615 normal_texture: None,
616 });
617
618 builder.add_material_unchecked(MaterialDef {
619 name: "Metal".to_string(),
620 albedo_color: [0.80, 0.80, 0.82, 1.0],
621 metallic: 0.95,
622 roughness: 0.20,
623 emissive: [0.0, 0.0, 0.0],
624 albedo_texture: None,
625 normal_texture: None,
626 });
627
628 builder.build()
629}
630
631fn collect_files_recursive(
637 base: &std::path::Path,
638 dir: &std::path::Path,
639 out: &mut Vec<(String, std::path::PathBuf)>,
640) -> Result<()> {
641 for entry in
642 std::fs::read_dir(dir).with_context(|| format!("reading dir: {}", dir.display()))?
643 {
644 let entry = entry.with_context(|| format!("iterating dir: {}", dir.display()))?;
645 let path = entry.path();
646 if path.is_dir() {
647 collect_files_recursive(base, &path, out)?;
648 } else if path.is_file() {
649 let rel = path
650 .strip_prefix(base)
651 .map_err(|e| anyhow::anyhow!("strip prefix {}: {e}", path.display()))?
652 .to_string_lossy()
653 .into_owned();
654 out.push((rel, path));
655 }
656 }
657 Ok(())
658}
659
660pub fn generate_distribution_manifest(pack_dir: &std::path::Path) -> Result<String> {
670 use sha2::{Digest, Sha256};
671
672 let mut pairs: Vec<(String, std::path::PathBuf)> = Vec::new();
673 collect_files_recursive(pack_dir, pack_dir, &mut pairs)?;
674 pairs.sort_by(|a, b| a.0.cmp(&b.0));
676
677 let mut entries: HashMap<String, String> = HashMap::new();
678 for (rel, abs) in pairs {
679 let data =
680 std::fs::read(&abs).with_context(|| format!("reading file: {}", abs.display()))?;
681 let hash = hex::encode(Sha256::digest(&data));
682 entries.insert(rel, hash);
683 }
684
685 let manifest = serde_json::json!({
686 "schema_version": "0.1.1",
687 "files": entries,
688 });
689 serde_json::to_string_pretty(&manifest).context("serializing distribution manifest")
690}
691
692pub fn verify_distribution_manifest(
702 manifest_json: &str,
703 pack_dir: &std::path::Path,
704) -> Result<bool> {
705 use sha2::{Digest, Sha256};
706
707 let manifest: serde_json::Value =
708 serde_json::from_str(manifest_json).context("parsing distribution manifest JSON")?;
709 let files = manifest["files"]
710 .as_object()
711 .ok_or_else(|| anyhow::anyhow!("manifest missing 'files' object"))?;
712
713 for (rel_path, expected_value) in files {
714 let expected = expected_value
715 .as_str()
716 .ok_or_else(|| anyhow::anyhow!("hash not a string for '{}'", rel_path))?;
717 let full_path = pack_dir.join(rel_path);
718 if !full_path.exists() {
719 return Ok(false);
720 }
721 let data = std::fs::read(&full_path)
722 .with_context(|| format!("reading file for verification: {}", full_path.display()))?;
723 let actual = hex::encode(Sha256::digest(&data));
724 if actual != expected {
725 return Ok(false);
726 }
727 }
728 Ok(true)
729}
730
731#[cfg(test)]
734mod tests {
735 use super::*;
736
737 fn make_1x1_texture(name: &str) -> TextureAsset {
740 TextureAsset {
741 name: name.to_string(),
742 width: 1,
743 height: 1,
744 channels: 3,
745 data: vec![255, 0, 0],
746 format: TextureFormat::Png,
747 }
748 }
749
750 fn make_simple_preset(name: &str) -> MorphPreset {
751 let mut params = HashMap::new();
752 params.insert("height_scale".to_string(), 1.0);
753 MorphPreset {
754 name: name.to_string(),
755 description: "Test preset".to_string(),
756 params,
757 tags: vec!["test".to_string()],
758 }
759 }
760
761 fn make_simple_material(name: &str) -> MaterialDef {
762 MaterialDef {
763 name: name.to_string(),
764 albedo_color: [0.8, 0.6, 0.4, 1.0],
765 metallic: 0.0,
766 roughness: 0.5,
767 emissive: [0.0, 0.0, 0.0],
768 albedo_texture: None,
769 normal_texture: None,
770 }
771 }
772
773 #[test]
775 fn empty_builder_produces_valid_oxp() {
776 let builder = AssetPackBuilder::new("empty-pack");
777 let bytes = builder.build().expect("should succeed");
778 assert!(!bytes.is_empty());
779 let index = load_pack_from_bytes(&bytes).expect("should succeed");
781 assert_eq!(index.name, "empty-pack");
782 }
783
784 #[test]
786 fn single_preset_round_trip() {
787 let mut builder = AssetPackBuilder::new("preset-pack");
788 builder
789 .add_preset(make_simple_preset("Alpha"))
790 .expect("should succeed");
791 let bytes = builder.build().expect("should succeed");
792 let index = load_pack_from_bytes(&bytes).expect("should succeed");
793 assert_eq!(index.presets.len(), 1);
794 assert_eq!(index.presets[0].name, "Alpha");
795 }
796
797 #[test]
799 fn multiple_presets_round_trip() {
800 let mut builder = AssetPackBuilder::new("multi-preset");
801 for name in &["A", "B", "C"] {
802 builder
803 .add_preset(make_simple_preset(name))
804 .expect("should succeed");
805 }
806 let bytes = builder.build().expect("should succeed");
807 let index = load_pack_from_bytes(&bytes).expect("should succeed");
808 assert_eq!(index.presets.len(), 3);
809 let names: Vec<&str> = index.presets.iter().map(|p| p.name.as_str()).collect();
810 assert!(names.contains(&"A"));
811 assert!(names.contains(&"B"));
812 assert!(names.contains(&"C"));
813 }
814
815 #[test]
817 fn texture_round_trip() {
818 let mut builder = AssetPackBuilder::new("tex-pack");
819 builder
820 .add_texture(make_1x1_texture("red"))
821 .expect("should succeed");
822 let bytes = builder.build().expect("should succeed");
823 let index = load_pack_from_bytes(&bytes).expect("should succeed");
824 assert_eq!(index.textures.len(), 1);
825 assert_eq!(index.textures[0].name, "red");
826 assert_eq!(index.textures[0].width, 1);
827 assert_eq!(index.textures[0].channels, 3);
828 }
829
830 #[test]
832 fn material_round_trip() {
833 let mut builder = AssetPackBuilder::new("mat-pack");
834 builder
835 .add_material(make_simple_material("chrome"))
836 .expect("should succeed");
837 let bytes = builder.build().expect("should succeed");
838 let index = load_pack_from_bytes(&bytes).expect("should succeed");
839 assert_eq!(index.materials.len(), 1);
840 assert_eq!(index.materials[0].name, "chrome");
841 assert!((index.materials[0].roughness - 0.5).abs() < 1e-6);
842 }
843
844 #[test]
846 fn mixed_entries_round_trip() {
847 let mut builder = AssetPackBuilder::new("mixed-pack");
848 builder
849 .add_preset(make_simple_preset("P1"))
850 .expect("should succeed");
851 builder
852 .add_texture(make_1x1_texture("T1"))
853 .expect("should succeed");
854 builder
855 .add_material(make_simple_material("M1"))
856 .expect("should succeed");
857 let bytes = builder.build().expect("should succeed");
858 let index = load_pack_from_bytes(&bytes).expect("should succeed");
859 assert_eq!(index.presets.len(), 1);
860 assert_eq!(index.textures.len(), 1);
861 assert_eq!(index.materials.len(), 1);
862 }
863
864 #[test]
866 fn target_delta_round_trip() {
867 let mut builder = AssetPackBuilder::new("delta-pack");
868 builder.add_target(TargetDelta {
869 name: "head_big.target".to_string(),
870 data: vec![1, 2, 3, 4, 5],
871 });
872 let bytes = builder.build().expect("should succeed");
873 let index = load_pack_from_bytes(&bytes).expect("should succeed");
874 assert_eq!(index.target_names.len(), 1);
875 assert_eq!(index.target_names[0], "head_big.target");
876 }
877
878 #[test]
880 fn alpha_pack_not_empty() {
881 let bytes = build_alpha_pack();
882 assert!(!bytes.is_empty(), "alpha pack must produce bytes");
883 }
884
885 #[test]
887 fn alpha_pack_has_five_presets() {
888 let bytes = build_alpha_pack();
889 let index = load_pack_from_bytes(&bytes).expect("should succeed");
890 assert_eq!(index.presets.len(), 5);
891 }
892
893 #[test]
895 fn alpha_pack_has_three_materials() {
896 let bytes = build_alpha_pack();
897 let index = load_pack_from_bytes(&bytes).expect("should succeed");
898 assert_eq!(index.materials.len(), 3);
899 }
900
901 #[test]
903 fn alpha_pack_preset_names() {
904 let bytes = build_alpha_pack();
905 let index = load_pack_from_bytes(&bytes).expect("should succeed");
906 let names: Vec<&str> = index.presets.iter().map(|p| p.name.as_str()).collect();
907 for expected in &["Athletic", "Slim", "Heavy", "Tall", "Short"] {
908 assert!(names.contains(expected), "missing preset: {}", expected);
909 }
910 }
911
912 #[test]
914 fn alpha_pack_material_names() {
915 let bytes = build_alpha_pack();
916 let index = load_pack_from_bytes(&bytes).expect("should succeed");
917 let names: Vec<&str> = index.materials.iter().map(|m| m.name.as_str()).collect();
918 for expected in &["Skin", "Cloth", "Metal"] {
919 assert!(names.contains(expected), "missing material: {}", expected);
920 }
921 }
922
923 #[test]
925 fn alpha_pack_manifest_author() {
926 let bytes = build_alpha_pack();
927 let index = load_pack_from_bytes(&bytes).expect("should succeed");
928 assert!(
929 index.author.contains("COOLJAPAN"),
930 "unexpected author: {}",
931 index.author
932 );
933 }
934
935 #[test]
937 fn alpha_pack_manifest_license() {
938 let bytes = build_alpha_pack();
939 let index = load_pack_from_bytes(&bytes).expect("should succeed");
940 assert!(!index.license.is_empty());
941 }
942
943 #[test]
945 fn alpha_pack_integrity_ok() {
946 let bytes = build_alpha_pack();
947 assert!(load_pack_from_bytes(&bytes).is_ok());
949 }
950
951 #[test]
953 fn texture_validation_wrong_length() {
954 let tex = TextureAsset {
955 name: "bad".to_string(),
956 width: 4,
957 height: 4,
958 channels: 3,
959 data: vec![0u8; 10], format: TextureFormat::Png,
961 };
962 assert!(tex.validate().is_err());
963 }
964
965 #[test]
967 fn texture_validation_zero_dimension() {
968 let tex = TextureAsset {
969 name: "bad".to_string(),
970 width: 0,
971 height: 1,
972 channels: 3,
973 data: vec![],
974 format: TextureFormat::Jpeg,
975 };
976 assert!(tex.validate().is_err());
977 }
978
979 #[test]
981 fn texture_validation_empty_name() {
982 let tex = TextureAsset {
983 name: String::new(),
984 width: 1,
985 height: 1,
986 channels: 3,
987 data: vec![0, 0, 0],
988 format: TextureFormat::Png,
989 };
990 assert!(tex.validate().is_err());
991 }
992
993 #[test]
995 fn material_validation_metallic_out_of_range() {
996 let mat = MaterialDef {
997 name: "bad".to_string(),
998 albedo_color: [1.0, 0.0, 0.0, 1.0],
999 metallic: 1.5,
1000 roughness: 0.5,
1001 emissive: [0.0; 3],
1002 albedo_texture: None,
1003 normal_texture: None,
1004 };
1005 assert!(mat.validate().is_err());
1006 }
1007
1008 #[test]
1010 fn material_validation_roughness_out_of_range() {
1011 let mat = MaterialDef {
1012 name: "bad".to_string(),
1013 albedo_color: [1.0, 0.0, 0.0, 1.0],
1014 metallic: 0.5,
1015 roughness: -0.1,
1016 emissive: [0.0; 3],
1017 albedo_texture: None,
1018 normal_texture: None,
1019 };
1020 assert!(mat.validate().is_err());
1021 }
1022
1023 #[test]
1025 fn material_validation_empty_name() {
1026 let mat = MaterialDef {
1027 name: String::new(),
1028 albedo_color: [1.0, 0.0, 0.0, 1.0],
1029 metallic: 0.0,
1030 roughness: 0.5,
1031 emissive: [0.0; 3],
1032 albedo_texture: None,
1033 normal_texture: None,
1034 };
1035 assert!(mat.validate().is_err());
1036 }
1037
1038 #[test]
1040 fn preset_validation_empty_name() {
1041 let preset = MorphPreset {
1042 name: String::new(),
1043 description: "desc".to_string(),
1044 params: HashMap::new(),
1045 tags: vec![],
1046 };
1047 assert!(preset.validate().is_err());
1048 }
1049
1050 #[test]
1052 fn index_total_bytes() {
1053 let mut builder = AssetPackBuilder::new("size-pack");
1054 builder
1055 .add_preset(make_simple_preset("X"))
1056 .expect("should succeed");
1057 let bytes = builder.build().expect("should succeed");
1058 let expected_len = bytes.len();
1059 let index = load_pack_from_bytes(&bytes).expect("should succeed");
1060 assert_eq!(index.total_bytes, expected_len);
1061 }
1062
1063 #[test]
1065 fn builder_metadata_in_index() {
1066 let mut builder = AssetPackBuilder::new("meta-pack");
1067 builder
1068 .set_author("Alice")
1069 .set_version("2.0.0")
1070 .set_license("MIT")
1071 .set_description("A test pack");
1072 let bytes = builder.build().expect("should succeed");
1073 let index = load_pack_from_bytes(&bytes).expect("should succeed");
1074 assert_eq!(index.author, "Alice");
1075 assert_eq!(index.version, "2.0.0");
1076 assert_eq!(index.license, "MIT");
1077 assert_eq!(index.description, "A test pack");
1078 }
1079
1080 #[test]
1082 fn corrupted_bytes_rejected() {
1083 let mut builder = AssetPackBuilder::new("pack");
1084 builder
1085 .add_preset(make_simple_preset("P"))
1086 .expect("should succeed");
1087 let mut bytes = builder.build().expect("should succeed");
1088 let mid = bytes.len() / 2;
1090 bytes[mid] ^= 0xFF;
1091 assert!(load_pack_from_bytes(&bytes).is_err());
1092 }
1093
1094 #[test]
1096 fn texture_format_helpers() {
1097 assert_eq!(TextureFormat::Png.extension(), "png");
1098 assert_eq!(TextureFormat::Jpeg.extension(), "jpg");
1099 assert_eq!(TextureFormat::Exr.extension(), "exr");
1100 assert_eq!(TextureFormat::Png.mime_type(), "image/png");
1101 }
1102
1103 #[test]
1105 fn preset_pack_paths_unique() {
1106 let p1 = make_simple_preset("Athletic Body");
1107 let p2 = make_simple_preset("Slim Body");
1108 assert_ne!(p1.pack_path(), p2.pack_path());
1109 }
1110
1111 #[test]
1113 fn alpha_pack_athletic_params() {
1114 let bytes = build_alpha_pack();
1115 let index = load_pack_from_bytes(&bytes).expect("should succeed");
1116 let athletic = index
1117 .presets
1118 .iter()
1119 .find(|p| p.name == "Athletic")
1120 .expect("Athletic preset not found");
1121 assert!(
1122 athletic.params.contains_key("muscle_mass"),
1123 "Athletic preset missing muscle_mass param"
1124 );
1125 let mm = athletic.params["muscle_mass"];
1126 assert!(mm > 0.5, "Athletic muscle_mass should be > 0.5, got {}", mm);
1127 }
1128
1129 #[test]
1131 fn alpha_pack_metal_material_metallic() {
1132 let bytes = build_alpha_pack();
1133 let index = load_pack_from_bytes(&bytes).expect("should succeed");
1134 let metal = index
1135 .materials
1136 .iter()
1137 .find(|m| m.name == "Metal")
1138 .expect("Metal material not found");
1139 assert!(
1140 metal.metallic > 0.9,
1141 "Metal material should have metallic > 0.9, got {}",
1142 metal.metallic
1143 );
1144 }
1145
1146 #[test]
1148 fn asset_pack_meta_defaults() {
1149 let meta = AssetPackMeta::default();
1150 assert_eq!(meta.version, "0.1.0");
1151 assert_eq!(meta.license, "Apache-2.0");
1152 assert_eq!(meta.created_at, 0);
1153 }
1154
1155 #[test]
1158 fn test_generate_and_verify_manifest() {
1159 let dir = std::env::temp_dir().join("oxihuman_dist_test_basic");
1160 std::fs::create_dir_all(&dir).expect("create dir");
1161 std::fs::write(dir.join("test.bin"), b"hello world").expect("write test.bin");
1162
1163 let manifest = generate_distribution_manifest(&dir).expect("generate manifest");
1164 assert!(
1165 manifest.contains("test.bin"),
1166 "manifest should reference test.bin"
1167 );
1168 assert!(
1169 manifest.contains("schema_version"),
1170 "manifest should have schema_version"
1171 );
1172
1173 let ok = verify_distribution_manifest(&manifest, &dir).expect("verify manifest");
1174 assert!(ok, "fresh manifest must verify successfully");
1175
1176 std::fs::remove_dir_all(&dir).ok();
1177 }
1178
1179 #[test]
1180 fn test_manifest_detects_tampered_file() {
1181 let dir = std::env::temp_dir().join("oxihuman_dist_test_tamper");
1182 std::fs::create_dir_all(&dir).expect("create dir");
1183 std::fs::write(dir.join("data.bin"), b"original").expect("write data.bin");
1184
1185 let manifest = generate_distribution_manifest(&dir).expect("generate manifest");
1186
1187 std::fs::write(dir.join("data.bin"), b"tampered!").expect("overwrite data.bin");
1189
1190 let ok = verify_distribution_manifest(&manifest, &dir).expect("verify call should not err");
1191 assert!(!ok, "tampered file must cause verification failure");
1192
1193 std::fs::remove_dir_all(&dir).ok();
1194 }
1195
1196 #[test]
1197 fn test_manifest_detects_missing_file() {
1198 let dir = std::env::temp_dir().join("oxihuman_dist_test_missing");
1199 std::fs::create_dir_all(&dir).expect("create dir");
1200 std::fs::write(dir.join("present.bin"), b"data").expect("write present.bin");
1201
1202 let manifest = generate_distribution_manifest(&dir).expect("generate manifest");
1203
1204 std::fs::remove_file(dir.join("present.bin")).ok();
1206
1207 let ok = verify_distribution_manifest(&manifest, &dir).expect("verify call should not err");
1208 assert!(!ok, "missing file must cause verification failure");
1209
1210 std::fs::remove_dir_all(&dir).ok();
1211 }
1212
1213 #[test]
1214 fn test_manifest_schema_version() {
1215 let dir = std::env::temp_dir().join("oxihuman_dist_test_schema");
1216 std::fs::create_dir_all(&dir).expect("create dir");
1217 std::fs::write(dir.join("x.bin"), b"x").expect("write x.bin");
1218
1219 let manifest = generate_distribution_manifest(&dir).expect("generate manifest");
1220 let v: serde_json::Value =
1221 serde_json::from_str(&manifest).expect("manifest must be valid JSON");
1222 assert_eq!(
1223 v["schema_version"]
1224 .as_str()
1225 .expect("schema_version must be string"),
1226 "0.1.1"
1227 );
1228
1229 std::fs::remove_dir_all(&dir).ok();
1230 }
1231
1232 #[test]
1233 fn test_manifest_multiple_files() {
1234 let dir = std::env::temp_dir().join("oxihuman_dist_test_multi");
1235 std::fs::create_dir_all(&dir).expect("create dir");
1236 std::fs::write(dir.join("a.bin"), b"alpha").expect("write a.bin");
1237 std::fs::write(dir.join("b.bin"), b"beta").expect("write b.bin");
1238 std::fs::write(dir.join("c.bin"), b"gamma").expect("write c.bin");
1239
1240 let manifest = generate_distribution_manifest(&dir).expect("generate manifest");
1241 let v: serde_json::Value =
1242 serde_json::from_str(&manifest).expect("manifest must be valid JSON");
1243 let files = v["files"].as_object().expect("files must be object");
1244 assert_eq!(files.len(), 3, "should have exactly 3 entries");
1245
1246 let ok = verify_distribution_manifest(&manifest, &dir).expect("verify");
1247 assert!(ok);
1248
1249 std::fs::remove_dir_all(&dir).ok();
1250 }
1251}