1use anyhow::{bail, Context, Result};
8use std::path::PathBuf;
9
10use oxihuman_core::policy::{Policy, PolicyProfile};
11use oxihuman_core::{
12 read_signature_file, sign_pack_dir, verify_pack_signature, write_signature_file,
13};
14use oxihuman_export::asset_bundle::{bundle_from_dir, export_bundle, AssetBundle as OxbBundle};
15use oxihuman_export::pack::{build_pack, PackBuilderConfig};
16use oxihuman_export::{
17 from_target_files, morph_delta_stats, pack_mesh_assets_with_options, quantize_mesh,
18 quantize_stats, write_morph_delta_bin, write_quantized_bin,
19};
20use oxihuman_mesh::MeshBuffers;
21
22pub(crate) fn partition_by_policy<'a>(
29 names: &'a [String],
30 policy: &Policy,
31) -> (Vec<&'a str>, Vec<&'a str>) {
32 let mut allowed = Vec::new();
33 let mut rejected = Vec::new();
34 for name in names {
35 if policy.is_target_allowed(name, &[]) {
36 allowed.push(name.as_str());
37 } else {
38 rejected.push(name.as_str());
39 }
40 }
41 (allowed, rejected)
42}
43
44pub(crate) fn warn_rejected(command: &str, rejected: &[&str]) {
46 if !rejected.is_empty() {
47 eprintln!(
48 "OxiHuman: {command}: {} target(s) rejected by policy: {}",
49 rejected.len(),
50 rejected.join(", ")
51 );
52 }
53}
54
55pub fn cmd_pack_build(args: &[String]) -> Result<()> {
58 let mut targets_dir: Option<PathBuf> = None;
59 let mut output: Option<PathBuf> = None;
60 let mut max_targets: Option<usize> = None;
61 let mut strict = false;
62
63 let mut i = 0;
64 while i < args.len() {
65 match args[i].as_str() {
66 "--targets" => {
67 i += 1;
68 targets_dir = Some(PathBuf::from(&args[i]));
69 }
70 "--output" => {
71 i += 1;
72 output = Some(PathBuf::from(&args[i]));
73 }
74 "--max-targets" => {
75 i += 1;
76 max_targets = Some(args[i].parse()?);
77 }
78 "--strict" => {
79 strict = true;
80 }
81 other => bail!("unknown option: {}", other),
82 }
83 i += 1;
84 }
85
86 let targets_dir = targets_dir.context("--targets is required for pack-build")?;
87 if !targets_dir.exists() {
88 bail!("targets directory not found: {}", targets_dir.display());
89 }
90
91 let policy = if strict {
92 Policy::new(PolicyProfile::Strict)
93 } else {
94 Policy::new(PolicyProfile::Standard)
95 };
96
97 eprintln!("OxiHuman: scanning {}...", targets_dir.display());
98 let manifest = build_pack(PackBuilderConfig {
99 targets_dir,
100 policy,
101 max_files: max_targets,
102 })?;
103
104 eprintln!(
105 "OxiHuman: {} files ({} allowed, {} blocked), {} deltas, ~{} KB",
106 manifest.stats.total_files,
107 manifest.stats.allowed_files,
108 manifest.stats.blocked_files,
109 manifest.stats.total_deltas,
110 manifest.stats.estimated_memory_bytes / 1024,
111 );
112
113 if let Some(out) = output {
114 manifest.write_to(&out)?;
115 eprintln!("OxiHuman: manifest -> {}", out.display());
116 } else {
117 println!("{}", manifest.to_toml()?);
118 }
119
120 Ok(())
121}
122
123pub fn cmd_quantize(args: &[String]) -> Result<()> {
126 use oxihuman_core::parser::obj::parse_obj;
127
128 let mut base: Option<PathBuf> = None;
129 let mut output: Option<PathBuf> = None;
130 let mut stats = false;
131 let mut strict = false;
132
133 let mut i = 0;
134 while i < args.len() {
135 match args[i].as_str() {
136 "--base" => {
137 i += 1;
138 base = Some(PathBuf::from(&args[i]));
139 }
140 "--output" => {
141 i += 1;
142 output = Some(PathBuf::from(&args[i]));
143 }
144 "--stats" => {
145 stats = true;
146 }
147 "--strict" => {
148 strict = true;
149 }
150 other => bail!("unknown option: {}", other),
151 }
152 i += 1;
153 }
154
155 let base = base.context("--base is required for quantize")?;
156 let output = output.context("--output is required for quantize")?;
157
158 if !base.exists() {
159 bail!("base mesh not found: {}", base.display());
160 }
161
162 let policy = if strict {
163 Policy::new(PolicyProfile::Strict)
164 } else {
165 Policy::new(PolicyProfile::Standard)
166 };
167 let base_name = base
168 .file_stem()
169 .and_then(|s| s.to_str())
170 .unwrap_or("base")
171 .to_string();
172 if !policy.is_target_allowed(&base_name, &[]) {
173 bail!(
174 "quantize: input '{}' rejected by policy (blocked tag in name)",
175 base_name
176 );
177 }
178
179 let src = std::fs::read_to_string(&base)
180 .with_context(|| format!("reading OBJ: {}", base.display()))?;
181 let obj = parse_obj(&src).context("parsing OBJ")?;
182 let morph_buf = oxihuman_morph::engine::MeshBuffers {
183 positions: obj.positions,
184 normals: obj.normals,
185 uvs: obj.uvs,
186 indices: obj.indices,
187 has_suit: false,
188 };
189 let mesh = MeshBuffers::from_morph(morph_buf);
190
191 let qmesh = quantize_mesh(&mesh);
192
193 if stats {
194 let qs = quantize_stats(&mesh, &qmesh);
195 println!("Quantize stats:");
196 println!(" position_error_rms: {:.6}", qs.position_error_rms);
197 println!(" normal_error_rms: {:.6}", qs.normal_error_rms);
198 println!(" uv_error_rms: {:.6}", qs.uv_error_rms);
199 println!(" compression_ratio: {:.3}x", qs.compression_ratio);
200 }
201
202 let bytes = write_quantized_bin(&qmesh, &output)
203 .with_context(|| format!("writing QMSH to {}", output.display()))?;
204
205 println!(
206 "Written QMSH: {} vertices, {} indices → {} bytes",
207 qmesh.positions.len(),
208 qmesh.indices.len(),
209 bytes
210 );
211
212 Ok(())
213}
214
215pub fn cmd_morph_export(args: &[String]) -> Result<()> {
218 use oxihuman_core::parser::obj::parse_obj;
219 use oxihuman_core::parser::target::parse_target;
220
221 let mut base: Option<PathBuf> = None;
222 let mut targets_dir: Option<PathBuf> = None;
223 let mut output: Option<PathBuf> = None;
224 let mut max_targets: Option<usize> = None;
225 let mut strict = false;
226
227 let mut i = 0;
228 while i < args.len() {
229 match args[i].as_str() {
230 "--base" => {
231 i += 1;
232 base = Some(PathBuf::from(&args[i]));
233 }
234 "--targets" => {
235 i += 1;
236 targets_dir = Some(PathBuf::from(&args[i]));
237 }
238 "--output" => {
239 i += 1;
240 output = Some(PathBuf::from(&args[i]));
241 }
242 "--max-targets" => {
243 i += 1;
244 max_targets = Some(args[i].parse()?);
245 }
246 "--strict" => {
247 strict = true;
248 }
249 other => bail!("unknown option: {}", other),
250 }
251 i += 1;
252 }
253
254 let base = base.context("--base is required for morph-export")?;
255 let targets_dir = targets_dir.context("--targets is required for morph-export")?;
256 let output = output.context("--output is required for morph-export")?;
257
258 if !base.exists() {
259 bail!("base mesh not found: {}", base.display());
260 }
261 if !targets_dir.exists() {
262 bail!("targets directory not found: {}", targets_dir.display());
263 }
264
265 let policy = if strict {
266 Policy::new(PolicyProfile::Strict)
267 } else {
268 Policy::new(PolicyProfile::Standard)
269 };
270
271 let src = std::fs::read_to_string(&base)
272 .with_context(|| format!("reading OBJ: {}", base.display()))?;
273 let obj = parse_obj(&src).context("parsing OBJ")?;
274 let vertex_count = obj.positions.len() as u32;
275
276 let mut entries = std::fs::read_dir(&targets_dir)
278 .with_context(|| format!("reading targets dir: {}", targets_dir.display()))?
279 .flatten()
280 .filter(|e| e.path().extension().map(|x| x == "target").unwrap_or(false))
281 .collect::<Vec<_>>();
282 entries.sort_by_key(|e| e.path());
283
284 if let Some(max) = max_targets {
285 entries.truncate(max);
286 }
287
288 let mut target_pairs: Vec<(String, oxihuman_core::parser::target::TargetFile)> = Vec::new();
289 let mut rejected_names: Vec<String> = Vec::new();
290 for entry in &entries {
291 let path = entry.path();
292 let name = path
293 .file_stem()
294 .and_then(|s| s.to_str())
295 .unwrap_or_default()
296 .to_string();
297 if !policy.is_target_allowed(&name, &[]) {
298 rejected_names.push(name);
299 continue;
300 }
301 let text = std::fs::read_to_string(&path)
302 .with_context(|| format!("reading target: {}", path.display()))?;
303 let tf = parse_target(&name, &text)
304 .with_context(|| format!("parsing target: {}", path.display()))?;
305 target_pairs.push((name, tf));
306 }
307 let rejected_refs: Vec<&str> = rejected_names.iter().map(|s| s.as_str()).collect();
308 warn_rejected("morph-export", &rejected_refs);
309
310 let ref_pairs: Vec<(String, &oxihuman_core::parser::target::TargetFile)> =
311 target_pairs.iter().map(|(n, t)| (n.clone(), t)).collect();
312
313 let bin = from_target_files(&ref_pairs, vertex_count);
314 let stats = morph_delta_stats(&bin);
315
316 write_morph_delta_bin(&bin, &output)
317 .with_context(|| format!("writing OXMD to {}", output.display()))?;
318
319 let file_size = std::fs::metadata(&output)?.len() as usize;
320
321 println!(
322 "Written OXMD: {} targets, {} total deltas → {} bytes",
323 bin.targets.len(),
324 stats.total_deltas,
325 file_size
326 );
327
328 Ok(())
329}
330
331pub fn cmd_zip_pack(args: &[String]) -> Result<()> {
334 use oxihuman_core::parser::obj::parse_obj;
335
336 let mut base: Option<PathBuf> = None;
337 let mut targets_dir: Option<PathBuf> = None;
338 let mut output: Option<PathBuf> = None;
339 let mut strict = false;
340 let mut deflate = false;
341
342 let mut i = 0;
343 while i < args.len() {
344 match args[i].as_str() {
345 "--base" => {
346 i += 1;
347 base = Some(PathBuf::from(&args[i]));
348 }
349 "--targets" => {
350 i += 1;
351 targets_dir = Some(PathBuf::from(&args[i]));
352 }
353 "--output" => {
354 i += 1;
355 output = Some(PathBuf::from(&args[i]));
356 }
357 "--strict" => {
358 strict = true;
359 }
360 "--deflate" => {
361 deflate = true;
362 }
363 other => bail!("unknown option: {}", other),
364 }
365 i += 1;
366 }
367
368 let base = base.context("--base is required for zip-pack")?;
369 let targets_dir = targets_dir.context("--targets is required for zip-pack")?;
370 let output = output.context("--output is required for zip-pack")?;
371
372 if !base.exists() {
373 bail!("base mesh not found: {}", base.display());
374 }
375 if !targets_dir.exists() {
376 bail!("targets directory not found: {}", targets_dir.display());
377 }
378
379 let policy = if strict {
380 Policy::new(PolicyProfile::Strict)
381 } else {
382 Policy::new(PolicyProfile::Standard)
383 };
384
385 let src = std::fs::read_to_string(&base)
387 .with_context(|| format!("reading OBJ: {}", base.display()))?;
388 let obj = parse_obj(&src).context("parsing OBJ")?;
389 let morph_buf = oxihuman_morph::engine::MeshBuffers {
390 positions: obj.positions,
391 normals: obj.normals,
392 uvs: obj.uvs,
393 indices: obj.indices,
394 has_suit: false,
395 };
396 let mut mesh = MeshBuffers::from_morph(morph_buf);
397
398 oxihuman_mesh::normals::compute_normals(&mut mesh);
400 oxihuman_mesh::suit::apply_suit_flag(&mut mesh);
401
402 let tmp_glb = output.with_extension("_tmp.glb");
404 oxihuman_export::glb::export_glb(&mesh, &tmp_glb)
405 .with_context(|| format!("exporting GLB to {}", tmp_glb.display()))?;
406 let glb_bytes = std::fs::read(&tmp_glb)?;
407 let _ = std::fs::remove_file(&tmp_glb);
408
409 let params_json = serde_json::to_vec(&serde_json::json!({
411 "base": base.display().to_string(),
412 "targets": targets_dir.display().to_string(),
413 }))?;
414
415 let all_target_files: Vec<(String, String)> = std::fs::read_dir(&targets_dir)
418 .with_context(|| format!("reading targets dir: {}", targets_dir.display()))?
419 .flatten()
420 .filter(|e| e.path().extension().map(|x| x == "target").unwrap_or(false))
421 .map(|e| {
422 let filename = e.file_name().to_string_lossy().into_owned();
423 let stem = e
424 .path()
425 .file_stem()
426 .and_then(|s| s.to_str())
427 .unwrap_or(&filename)
428 .to_string();
429 (filename, stem)
430 })
431 .collect();
432 let mut target_names: Vec<String> = Vec::new();
433 let mut rejected_names: Vec<String> = Vec::new();
434 for (filename, stem) in &all_target_files {
435 if policy.is_target_allowed(stem, &[]) {
436 target_names.push(filename.clone());
437 } else {
438 rejected_names.push(stem.clone());
439 }
440 }
441 let rejected_refs: Vec<&str> = rejected_names.iter().map(|s| s.as_str()).collect();
442 warn_rejected("zip-pack", &rejected_refs);
443 let manifest_json = serde_json::to_vec(&serde_json::json!({ "targets": target_names }))?;
444
445 let result =
446 pack_mesh_assets_with_options(&glb_bytes, ¶ms_json, &manifest_json, &output, deflate)
447 .with_context(|| format!("writing ZIP to {}", output.display()))?;
448
449 println!(
450 "Written ZIP pack: {} entries → {}",
451 result.entry_count,
452 output.display()
453 );
454
455 Ok(())
456}
457
458#[allow(dead_code)]
461pub fn cmd_asset_bundle(args: &[String]) -> Result<()> {
462 use oxihuman_core::parser::obj::parse_obj;
463
464 let mut base: Option<PathBuf> = None;
465 let mut targets_dir: Option<PathBuf> = None;
466 let mut output: Option<PathBuf> = None;
467 let mut manifest: Option<PathBuf> = None;
468 let mut strict = false;
469
470 let mut i = 0;
471 while i < args.len() {
472 match args[i].as_str() {
473 "--base" => {
474 i += 1;
475 base = Some(PathBuf::from(&args[i]));
476 }
477 "--targets" => {
478 i += 1;
479 targets_dir = Some(PathBuf::from(&args[i]));
480 }
481 "--output" => {
482 i += 1;
483 output = Some(PathBuf::from(&args[i]));
484 }
485 "--manifest" => {
486 i += 1;
487 manifest = Some(PathBuf::from(&args[i]));
488 }
489 "--strict" => {
490 strict = true;
491 }
492 other => bail!("unknown option: {}", other),
493 }
494 i += 1;
495 }
496
497 let base = base.context("--base is required for asset-bundle")?;
498 let targets_dir = targets_dir.context("--targets is required for asset-bundle")?;
499 let output = output.context("--output is required for asset-bundle")?;
500
501 if !base.exists() {
502 bail!("base mesh not found: {}", base.display());
503 }
504 if !targets_dir.exists() {
505 bail!("targets directory not found: {}", targets_dir.display());
506 }
507
508 let policy = if strict {
509 Policy::new(PolicyProfile::Strict)
510 } else {
511 Policy::new(PolicyProfile::Standard)
512 };
513
514 let obj_bytes =
516 std::fs::read(&base).with_context(|| format!("reading OBJ: {}", base.display()))?;
517
518 let mut bundle = OxbBundle::new();
520 let base_name = base
521 .file_name()
522 .and_then(|n| n.to_str())
523 .unwrap_or("base.obj")
524 .to_string();
525 bundle
526 .add_bytes(&base_name, obj_bytes)
527 .with_context(|| format!("adding base OBJ '{}' to bundle", base_name))?;
528
529 let obj_src = std::fs::read_to_string(&base)
531 .with_context(|| format!("reading OBJ: {}", base.display()))?;
532 let _obj = parse_obj(&obj_src).context("parsing OBJ")?;
533
534 let target_bundle = bundle_from_dir(&targets_dir)
537 .with_context(|| format!("scanning targets dir: {}", targets_dir.display()))?;
538 let target_count = target_bundle.entry_count();
539 let mut rejected_names: Vec<String> = Vec::new();
540 for name in target_bundle.entry_names() {
541 let stem = std::path::Path::new(name)
542 .file_stem()
543 .and_then(|s| s.to_str())
544 .unwrap_or(name);
545 if !policy.is_target_allowed(stem, &[]) {
546 rejected_names.push(stem.to_string());
547 continue;
548 }
549 if let Some(entry) = target_bundle.get(name) {
550 let entry_data = entry.data.clone();
551 let bundle_name = format!("targets/{}", name);
553 bundle.add_bytes(bundle_name, entry_data).ok(); }
555 }
556 let rejected_refs: Vec<&str> = rejected_names.iter().map(|s| s.as_str()).collect();
557 warn_rejected("asset-bundle", &rejected_refs);
558
559 if let Some(ref mp) = manifest {
561 if !mp.exists() {
562 bail!("manifest file not found: {}", mp.display());
563 }
564 let manifest_bytes =
565 std::fs::read(mp).with_context(|| format!("reading manifest: {}", mp.display()))?;
566 bundle.add_bytes("manifest.toml", manifest_bytes).ok();
567 }
568
569 let total_assets = bundle.entry_count();
570 export_bundle(&bundle, &output)
571 .with_context(|| format!("writing bundle: {}", output.display()))?;
572
573 println!(
574 "Written bundle: {} assets → {}",
575 total_assets,
576 output.display()
577 );
578 let _ = target_count; Ok(())
580}
581
582#[allow(dead_code)]
585pub fn cmd_validate_pack(args: &[String]) -> Result<()> {
586 use oxihuman_core::{scan_pack, verify_manifest_present, verify_pack};
587
588 let mut pack_dir: Option<PathBuf> = None;
589
590 let mut i = 0;
591 while i < args.len() {
592 match args[i].as_str() {
593 "--pack-dir" => {
594 i += 1;
595 pack_dir = Some(PathBuf::from(&args[i]));
596 }
597 other => bail!("unknown option: {}", other),
598 }
599 i += 1;
600 }
601
602 let pack_dir = pack_dir.context("--pack-dir is required for validate-pack")?;
603
604 if !pack_dir.exists() {
605 bail!("pack directory not found: {}", pack_dir.display());
606 }
607
608 match verify_manifest_present(&pack_dir) {
610 Ok(()) => println!("Manifest: present"),
611 Err(e) => println!("Manifest: MISSING — {}", e),
612 }
613
614 let records =
616 scan_pack(&pack_dir).with_context(|| format!("scanning pack: {}", pack_dir.display()))?;
617 println!("Scanned {} file(s)", records.len());
618
619 let report = verify_pack(&pack_dir, &records);
620 println!("{}", report.summary());
621
622 if !report.is_valid {
623 if !report.failed_files.is_empty() {
624 println!("Failed files:");
625 for f in &report.failed_files {
626 println!(" FAIL: {}", f);
627 }
628 }
629 if !report.missing_files.is_empty() {
630 println!("Missing files:");
631 for f in &report.missing_files {
632 println!(" MISS: {}", f);
633 }
634 }
635 bail!("pack validation failed");
636 }
637 Ok(())
638}
639
640#[allow(dead_code)]
643pub fn cmd_sign_pack(args: &[String]) -> Result<()> {
644 let mut pack_dir: Option<PathBuf> = None;
645 let mut key_str: Option<String> = None;
646 let mut signer_id = String::from("oxihuman-cli");
647 let mut output: Option<PathBuf> = None;
648
649 let mut i = 0;
650 while i < args.len() {
651 match args[i].as_str() {
652 "--pack-dir" => {
653 i += 1;
654 pack_dir = Some(PathBuf::from(&args[i]));
655 }
656 "--key" => {
657 i += 1;
658 key_str = Some(args[i].clone());
659 }
660 "--signer-id" => {
661 i += 1;
662 signer_id = args[i].clone();
663 }
664 "--output" => {
665 i += 1;
666 output = Some(PathBuf::from(&args[i]));
667 }
668 other => bail!("sign-pack: unknown option: {}", other),
669 }
670 i += 1;
671 }
672
673 let pack_dir = pack_dir.context("--pack-dir is required for sign-pack")?;
674 let key_str = key_str.context("--key is required for sign-pack")?;
675 let output = output.context("--output is required for sign-pack")?;
676
677 if !pack_dir.exists() {
678 bail!("pack-dir not found: {}", pack_dir.display());
679 }
680
681 let signed = sign_pack_dir(&pack_dir, key_str.as_bytes(), &signer_id)
682 .with_context(|| format!("signing pack dir: {}", pack_dir.display()))?;
683 write_signature_file(&signed, &output)
684 .with_context(|| format!("writing signature file: {}", output.display()))?;
685 println!("Pack signed. Signature written to: {}", output.display());
686 Ok(())
687}
688
689#[allow(dead_code)]
692pub fn cmd_verify_sign(args: &[String]) -> Result<()> {
693 let mut pack_dir: Option<PathBuf> = None;
694 let mut sig_file: Option<PathBuf> = None;
695 let mut key_str: Option<String> = None;
696
697 let mut i = 0;
698 while i < args.len() {
699 match args[i].as_str() {
700 "--pack-dir" => {
701 i += 1;
702 pack_dir = Some(PathBuf::from(&args[i]));
703 }
704 "--sig-file" => {
705 i += 1;
706 sig_file = Some(PathBuf::from(&args[i]));
707 }
708 "--key" => {
709 i += 1;
710 key_str = Some(args[i].clone());
711 }
712 other => bail!("verify-sign: unknown option: {}", other),
713 }
714 i += 1;
715 }
716
717 let pack_dir = pack_dir.context("--pack-dir is required for verify-sign")?;
718 let sig_file = sig_file.context("--sig-file is required for verify-sign")?;
719 let key_str = key_str.context("--key is required for verify-sign")?;
720
721 if !pack_dir.exists() {
722 bail!("pack-dir not found: {}", pack_dir.display());
723 }
724 if !sig_file.exists() {
725 bail!("sig-file not found: {}", sig_file.display());
726 }
727
728 let signed = read_signature_file(&sig_file)
729 .with_context(|| format!("reading sig file: {}", sig_file.display()))?;
730 if verify_pack_signature(&pack_dir, &signed, key_str.as_bytes()) {
731 println!("VALID");
732 } else {
733 println!("INVALID");
734 }
735 Ok(())
736}
737
738#[allow(dead_code)]
746pub fn cmd_pack_dist_manifest(args: &[String]) -> Result<()> {
747 let mut pack_dir: Option<PathBuf> = None;
748
749 let mut i = 0;
750 while i < args.len() {
751 match args[i].as_str() {
752 "--pack-dir" => {
753 i += 1;
754 if i >= args.len() {
755 bail!("pack-dist-manifest: --pack-dir requires an argument");
756 }
757 pack_dir = Some(PathBuf::from(&args[i]));
758 }
759 other => bail!("pack-dist-manifest: unknown option: {}", other),
760 }
761 i += 1;
762 }
763
764 let pack_dir = pack_dir.context("--pack-dir is required for pack-dist-manifest")?;
765 if !pack_dir.exists() {
766 bail!("pack-dir not found: {}", pack_dir.display());
767 }
768
769 let manifest = oxihuman_core::asset_pack_builder::generate_distribution_manifest(&pack_dir)
770 .with_context(|| {
771 format!(
772 "generating distribution manifest for: {}",
773 pack_dir.display()
774 )
775 })?;
776 println!("{manifest}");
777 Ok(())
778}
779
780#[allow(dead_code)]
788pub fn cmd_pack_verify_dist(args: &[String]) -> Result<()> {
789 let mut manifest_path: Option<PathBuf> = None;
790 let mut pack_dir: Option<PathBuf> = None;
791
792 let mut i = 0;
793 while i < args.len() {
794 match args[i].as_str() {
795 "--manifest" => {
796 i += 1;
797 if i >= args.len() {
798 bail!("pack-verify-dist: --manifest requires an argument");
799 }
800 manifest_path = Some(PathBuf::from(&args[i]));
801 }
802 "--pack-dir" => {
803 i += 1;
804 if i >= args.len() {
805 bail!("pack-verify-dist: --pack-dir requires an argument");
806 }
807 pack_dir = Some(PathBuf::from(&args[i]));
808 }
809 other => bail!("pack-verify-dist: unknown option: {}", other),
810 }
811 i += 1;
812 }
813
814 let manifest_path = manifest_path.context("--manifest is required for pack-verify-dist")?;
815 let pack_dir = pack_dir.context("--pack-dir is required for pack-verify-dist")?;
816
817 if !manifest_path.exists() {
818 bail!("manifest file not found: {}", manifest_path.display());
819 }
820 if !pack_dir.exists() {
821 bail!("pack-dir not found: {}", pack_dir.display());
822 }
823
824 let json = std::fs::read_to_string(&manifest_path)
825 .with_context(|| format!("reading manifest: {}", manifest_path.display()))?;
826
827 let ok = oxihuman_core::asset_pack_builder::verify_distribution_manifest(&json, &pack_dir)
828 .with_context(|| {
829 format!(
830 "verifying distribution manifest against: {}",
831 pack_dir.display()
832 )
833 })?;
834
835 if ok {
836 println!("Manifest verification: OK");
837 } else {
838 eprintln!("Manifest verification: FAILED");
839 std::process::exit(1);
840 }
841 Ok(())
842}