lib3mf_cli/
commands.rs

1//! Command implementations for the 3mf CLI tool.
2//!
3//! This module contains the core logic for all CLI commands. Each public function
4//! corresponds to a CLI subcommand and can be called programmatically.
5
6pub mod thumbnails;
7
8use clap::ValueEnum;
9use lib3mf_core::archive::{find_model_path, opc, ArchiveReader, ZipArchiver};
10use lib3mf_core::parser::parse_model;
11use serde::Serialize;
12use std::collections::BTreeMap;
13use std::fs::File;
14use std::io::{Read, Seek, Write};
15use std::path::PathBuf;
16
17/// Output format for CLI commands.
18///
19/// Controls how command results are displayed to the user.
20#[derive(Clone, ValueEnum, Debug, PartialEq)]
21pub enum OutputFormat {
22    /// Human-readable text output (default)
23    Text,
24    /// JSON output for machine parsing
25    Json,
26    /// Tree-structured visualization
27    Tree,
28}
29
30/// Types of mesh repair operations.
31///
32/// Specifies which geometric repairs to perform on 3MF meshes.
33#[derive(Clone, ValueEnum, Debug, PartialEq, Copy)]
34pub enum RepairType {
35    /// Remove degenerate triangles (zero area)
36    Degenerate,
37    /// Remove duplicate triangles
38    Duplicates,
39    /// Harmonize triangle winding
40    Harmonize,
41    /// Remove disconnected components (islands)
42    Islands,
43    /// Attempt to fill holes (boundary loops)
44    Holes,
45    /// Perform all repairs
46    All,
47}
48
49enum ModelSource {
50    Archive(ZipArchiver<File>, lib3mf_core::model::Model),
51    Raw(lib3mf_core::model::Model),
52}
53
54fn open_model(path: &PathBuf) -> anyhow::Result<ModelSource> {
55    let mut file =
56        File::open(path).map_err(|e| anyhow::anyhow!("Failed to open file {:?}: {}", path, e))?;
57
58    let mut magic = [0u8; 4];
59    let is_zip = file.read_exact(&mut magic).is_ok() && &magic == b"PK\x03\x04";
60    file.rewind()?;
61
62    if is_zip {
63        let mut archiver = ZipArchiver::new(file)
64            .map_err(|e| anyhow::anyhow!("Failed to open zip archive: {}", e))?;
65        let model_path = find_model_path(&mut archiver)
66            .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
67        let model_data = archiver
68            .read_entry(&model_path)
69            .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
70        let model = parse_model(std::io::Cursor::new(model_data))
71            .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
72        Ok(ModelSource::Archive(archiver, model))
73    } else {
74        let ext = path
75            .extension()
76            .and_then(|s| s.to_str())
77            .unwrap_or("")
78            .to_lowercase();
79
80        match ext.as_str() {
81            "stl" => {
82                let model = lib3mf_converters::stl::StlImporter::read(file)
83                    .map_err(|e| anyhow::anyhow!("Failed to import STL: {}", e))?;
84                Ok(ModelSource::Raw(model))
85            }
86            "obj" => {
87                let model = lib3mf_converters::obj::ObjImporter::read(file)
88                    .map_err(|e| anyhow::anyhow!("Failed to import OBJ: {}", e))?;
89                Ok(ModelSource::Raw(model))
90            }
91            _ => Err(anyhow::anyhow!(
92                "Unsupported format: {} (and not a ZIP/3MF archive)",
93                ext
94            )),
95        }
96    }
97}
98
99/// Generate statistics and metadata for a 3MF file.
100///
101/// Computes and reports key metrics including unit of measurement, geometry counts,
102/// material groups, metadata, and system information.
103///
104/// # Arguments
105///
106/// * `path` - Path to the 3MF file or supported format (STL, OBJ)
107/// * `format` - Output format (Text, Json, or Tree visualization)
108///
109/// # Errors
110///
111/// Returns an error if the file cannot be opened, parsed, or if statistics computation fails.
112///
113/// # Example
114///
115/// ```no_run
116/// use lib3mf_cli::commands::{stats, OutputFormat};
117/// use std::path::PathBuf;
118///
119/// # fn main() -> anyhow::Result<()> {
120/// stats(PathBuf::from("model.3mf"), OutputFormat::Text)?;
121/// # Ok(())
122/// # }
123/// ```
124pub fn stats(path: PathBuf, format: OutputFormat) -> anyhow::Result<()> {
125    let mut source = open_model(&path)?;
126    let stats = match source {
127        ModelSource::Archive(ref mut archiver, ref model) => model
128            .compute_stats(archiver)
129            .map_err(|e| anyhow::anyhow!("Failed to compute stats: {}", e))?,
130        ModelSource::Raw(ref model) => {
131            struct NoArchive;
132            impl std::io::Read for NoArchive {
133                fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
134                    Ok(0)
135                }
136            }
137            impl std::io::Seek for NoArchive {
138                fn seek(&mut self, _: std::io::SeekFrom) -> std::io::Result<u64> {
139                    Ok(0)
140                }
141            }
142            impl lib3mf_core::archive::ArchiveReader for NoArchive {
143                fn read_entry(&mut self, _: &str) -> lib3mf_core::error::Result<Vec<u8>> {
144                    Err(lib3mf_core::error::Lib3mfError::Io(std::io::Error::new(
145                        std::io::ErrorKind::NotFound,
146                        "Raw format",
147                    )))
148                }
149                fn entry_exists(&mut self, _: &str) -> bool {
150                    false
151                }
152                fn list_entries(&mut self) -> lib3mf_core::error::Result<Vec<String>> {
153                    Ok(vec![])
154                }
155            }
156            model
157                .compute_stats(&mut NoArchive)
158                .map_err(|e| anyhow::anyhow!("Failed to compute stats: {}", e))?
159        }
160    };
161
162    match format {
163        OutputFormat::Json => {
164            println!("{}", serde_json::to_string_pretty(&stats)?);
165        }
166        OutputFormat::Tree => {
167            println!("Model Hierarchy for {:?}", path);
168            match source {
169                ModelSource::Archive(mut archiver, model) => {
170                    let mut resolver =
171                        lib3mf_core::model::resolver::PartResolver::new(&mut archiver, model);
172                    print_model_hierarchy_resolved(&mut resolver);
173                }
174                ModelSource::Raw(model) => {
175                    print_model_hierarchy(&model);
176                }
177            }
178        }
179        _ => {
180            println!("Stats for {:?}", path);
181            println!(
182                "Unit: {:?} (Scale: {} m)",
183                stats.unit,
184                stats.unit.scale_factor()
185            );
186            println!("Generator: {:?}", stats.generator.unwrap_or_default());
187            println!("Geometry:");
188
189            // Display object counts by type per CONTEXT.md decision
190            let type_display: Vec<String> =
191                ["model", "support", "solidsupport", "surface", "other"]
192                    .iter()
193                    .filter_map(|&type_name| {
194                        stats
195                            .geometry
196                            .type_counts
197                            .get(type_name)
198                            .and_then(|&count| {
199                                if count > 0 {
200                                    Some(format!("{} {}", count, type_name))
201                                } else {
202                                    None
203                                }
204                            })
205                    })
206                    .collect();
207
208            if type_display.is_empty() {
209                println!("  Objects: 0");
210            } else {
211                println!("  Objects: {}", type_display.join(", "));
212            }
213
214            println!("  Instances: {}", stats.geometry.instance_count);
215            println!("  Vertices: {}", stats.geometry.vertex_count);
216            println!("  Triangles: {}", stats.geometry.triangle_count);
217            if let Some(bbox) = stats.geometry.bounding_box {
218                println!("  Bounding Box: Min {:?}, Max {:?}", bbox.min, bbox.max);
219            }
220            let scale = stats.unit.scale_factor();
221            println!(
222                "  Surface Area: {:.2} (native units^2)",
223                stats.geometry.surface_area
224            );
225            println!(
226                "                {:.6} m^2",
227                stats.geometry.surface_area * scale * scale
228            );
229            println!(
230                "  Volume:       {:.2} (native units^3)",
231                stats.geometry.volume
232            );
233            println!(
234                "                {:.6} m^3",
235                stats.geometry.volume * scale * scale * scale
236            );
237
238            println!("\nSystem Info:");
239            println!("  Architecture: {}", stats.system_info.architecture);
240            println!("  CPUs (Threads): {}", stats.system_info.num_cpus);
241            println!(
242                "  SIMD Features: {}",
243                stats.system_info.simd_features.join(", ")
244            );
245
246            println!("Materials:");
247            println!("  Base Groups: {}", stats.materials.base_materials_count);
248            println!("  Color Groups: {}", stats.materials.color_groups_count);
249            println!(
250                "  Texture 2D Groups: {}",
251                stats.materials.texture_2d_groups_count
252            );
253            println!(
254                "  Composite Materials: {}",
255                stats.materials.composite_materials_count
256            );
257            println!(
258                "  Multi Properties: {}",
259                stats.materials.multi_properties_count
260            );
261
262            if !stats.vendor.plates.is_empty() {
263                println!("Vendor Data (Bambu):");
264                println!("  Plates: {}", stats.vendor.plates.len());
265                for plate in stats.vendor.plates {
266                    println!("    - ID {}: {}", plate.id, plate.name.unwrap_or_default());
267                }
268            }
269
270            println!("Thumbnails:");
271            println!(
272                "  Package Thumbnail: {}",
273                if stats.thumbnails.package_thumbnail_present {
274                    "Yes"
275                } else {
276                    "No"
277                }
278            );
279            println!(
280                "  Object Thumbnails: {}",
281                stats.thumbnails.object_thumbnail_count
282            );
283
284            // Displacement section
285            if stats.displacement.mesh_count > 0 || stats.displacement.texture_count > 0 {
286                println!("\nDisplacement:");
287                println!("  Meshes: {}", stats.displacement.mesh_count);
288                println!("  Textures: {}", stats.displacement.texture_count);
289                if stats.displacement.normal_count > 0 {
290                    println!("  Vertex Normals: {}", stats.displacement.normal_count);
291                }
292                if stats.displacement.gradient_count > 0 {
293                    println!("  Gradient Vectors: {}", stats.displacement.gradient_count);
294                }
295                if stats.displacement.total_triangle_count > 0 {
296                    let coverage = 100.0 * stats.displacement.displaced_triangle_count as f64
297                        / stats.displacement.total_triangle_count as f64;
298                    println!(
299                        "  Displaced Triangles: {} of {} ({:.1}%)",
300                        stats.displacement.displaced_triangle_count,
301                        stats.displacement.total_triangle_count,
302                        coverage
303                    );
304                }
305            }
306        }
307    }
308    Ok(())
309}
310
311/// List all entries in a 3MF archive.
312///
313/// Displays all files contained within the 3MF OPC (ZIP) archive in flat or tree format.
314///
315/// # Arguments
316///
317/// * `path` - Path to the 3MF file
318/// * `format` - Output format (Text for flat list, Json for structured, Tree for directory view)
319///
320/// # Errors
321///
322/// Returns an error if the archive cannot be opened or entries cannot be listed.
323pub fn list(path: PathBuf, format: OutputFormat) -> anyhow::Result<()> {
324    let source = open_model(&path)?;
325
326    let entries = match source {
327        ModelSource::Archive(mut archiver, _) => archiver
328            .list_entries()
329            .map_err(|e| anyhow::anyhow!("Failed to list entries: {}", e))?,
330        ModelSource::Raw(_) => vec![path
331            .file_name()
332            .and_then(|n| n.to_str())
333            .unwrap_or("model")
334            .to_string()],
335    };
336
337    match format {
338        OutputFormat::Json => {
339            let tree = build_file_tree(&entries);
340            println!("{}", serde_json::to_string_pretty(&tree)?);
341        }
342        OutputFormat::Tree => {
343            print_tree(&entries);
344        }
345        OutputFormat::Text => {
346            for entry in entries {
347                println!("{}", entry);
348            }
349        }
350    }
351    Ok(())
352}
353
354/// Inspect OPC relationships and content types.
355///
356/// Dumps the Open Packaging Convention (OPC) relationships from `_rels/.rels` and
357/// content types from `[Content_Types].xml`.
358///
359/// # Arguments
360///
361/// * `path` - Path to the 3MF file
362/// * `format` - Output format (Text or Json)
363///
364/// # Errors
365///
366/// Returns an error if the archive cannot be opened.
367pub fn rels(path: PathBuf, format: OutputFormat) -> anyhow::Result<()> {
368    let mut archiver = open_archive(&path)?;
369
370    // Read relationships
371    let rels_data = archiver.read_entry("_rels/.rels").unwrap_or_default();
372    let rels = if !rels_data.is_empty() {
373        opc::parse_relationships(&rels_data).unwrap_or_default()
374    } else {
375        Vec::new()
376    };
377
378    // Read content types
379    let types_data = archiver
380        .read_entry("[Content_Types].xml")
381        .unwrap_or_default();
382    let types = if !types_data.is_empty() {
383        opc::parse_content_types(&types_data).unwrap_or_default()
384    } else {
385        Vec::new()
386    };
387
388    match format {
389        OutputFormat::Json => {
390            #[derive(Serialize)]
391            struct OpcData {
392                relationships: Vec<lib3mf_core::archive::opc::Relationship>,
393                content_types: Vec<lib3mf_core::archive::opc::ContentType>,
394            }
395            let data = OpcData {
396                relationships: rels,
397                content_types: types,
398            };
399            println!("{}", serde_json::to_string_pretty(&data)?);
400        }
401        _ => {
402            println!("Relationships:");
403            for rel in rels {
404                println!(
405                    "  - ID: {}, Type: {}, Target: {}",
406                    rel.id, rel.rel_type, rel.target
407                );
408            }
409            println!("\nContent Types:");
410            for ct in types {
411                println!("  - {:?}", ct);
412            }
413        }
414    }
415    Ok(())
416}
417
418/// Dump the raw parsed Model structure for debugging.
419///
420/// Outputs the in-memory representation of the 3MF model for developer inspection.
421///
422/// # Arguments
423///
424/// * `path` - Path to the 3MF file
425/// * `format` - Output format (Text for debug format, Json for structured)
426///
427/// # Errors
428///
429/// Returns an error if the file cannot be parsed.
430pub fn dump(path: PathBuf, format: OutputFormat) -> anyhow::Result<()> {
431    let mut archiver = open_archive(&path)?;
432    let model_path = find_model_path(&mut archiver)
433        .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
434    let model_data = archiver
435        .read_entry(&model_path)
436        .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
437    let model = parse_model(std::io::Cursor::new(model_data))
438        .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
439
440    match format {
441        OutputFormat::Json => {
442            println!("{}", serde_json::to_string_pretty(&model)?);
443        }
444        _ => {
445            println!("{:#?}", model);
446        }
447    }
448    Ok(())
449}
450
451/// Extract a file from the 3MF archive by path.
452///
453/// Copies a specific file from inside the ZIP archive to the local filesystem or stdout.
454///
455/// # Arguments
456///
457/// * `path` - Path to the 3MF file
458/// * `inner_path` - Path to the file inside the archive
459/// * `output` - Output path (None = stdout)
460///
461/// # Errors
462///
463/// Returns an error if the archive cannot be opened or the entry doesn't exist.
464pub fn extract(path: PathBuf, inner_path: String, output: Option<PathBuf>) -> anyhow::Result<()> {
465    let mut archiver = open_archive(&path)?;
466    let data = archiver
467        .read_entry(&inner_path)
468        .map_err(|e| anyhow::anyhow!("Failed to read entry '{}': {}", inner_path, e))?;
469
470    if let Some(out_path) = output {
471        let mut f = File::create(&out_path)
472            .map_err(|e| anyhow::anyhow!("Failed to create output file {:?}: {}", out_path, e))?;
473        f.write_all(&data)?;
474        println!("Extracted '{}' to {:?}", inner_path, out_path);
475    } else {
476        std::io::stdout().write_all(&data)?;
477    }
478    Ok(())
479}
480
481/// Extract a texture resource by resource ID.
482///
483/// Extracts displacement or texture resources by their ID rather than archive path.
484///
485/// # Arguments
486///
487/// * `path` - Path to the 3MF file
488/// * `resource_id` - Resource ID of the texture (Displacement2D or Texture2D)
489/// * `output` - Output path (None = stdout)
490///
491/// # Errors
492///
493/// Returns an error if the resource doesn't exist or cannot be extracted.
494pub fn extract_by_resource_id(
495    path: PathBuf,
496    resource_id: u32,
497    output: Option<PathBuf>,
498) -> anyhow::Result<()> {
499    let mut archiver = open_archive(&path)?;
500    let model_path = find_model_path(&mut archiver)?;
501    let model_data = archiver.read_entry(&model_path)?;
502    let model = parse_model(std::io::Cursor::new(model_data))?;
503
504    let resource_id = lib3mf_core::model::ResourceId(resource_id);
505
506    // Look up Displacement2D resource by ID
507    if let Some(disp2d) = model.resources.get_displacement_2d(resource_id) {
508        let texture_path = &disp2d.path;
509        let archive_path = texture_path.trim_start_matches('/');
510        let data = archiver
511            .read_entry(archive_path)
512            .map_err(|e| anyhow::anyhow!("Failed to read texture '{}': {}", archive_path, e))?;
513
514        if let Some(out_path) = output {
515            let mut f = File::create(&out_path)?;
516            f.write_all(&data)?;
517            println!(
518                "Extracted displacement texture (ID {}) to {:?}",
519                resource_id.0, out_path
520            );
521        } else {
522            std::io::stdout().write_all(&data)?;
523        }
524        return Ok(());
525    }
526
527    Err(anyhow::anyhow!(
528        "No displacement texture resource found with ID {}",
529        resource_id.0
530    ))
531}
532
533/// Copy and re-package a 3MF file.
534///
535/// Reads the input file, parses it into memory, and writes it back to a new file.
536/// This verifies that lib3mf can successfully parse and re-serialize the model.
537///
538/// # Arguments
539///
540/// * `input` - Input 3MF file path
541/// * `output` - Output 3MF file path
542///
543/// # Errors
544///
545/// Returns an error if parsing or writing fails.
546pub fn copy(input: PathBuf, output: PathBuf) -> anyhow::Result<()> {
547    let mut archiver = open_archive(&input)?;
548    let model_path = find_model_path(&mut archiver)
549        .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
550    let model_data = archiver
551        .read_entry(&model_path)
552        .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
553    let mut model = parse_model(std::io::Cursor::new(model_data))
554        .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
555
556    // Load all existing files to preserve multi-part relationships and attachments
557    let all_files = archiver.list_entries()?;
558    for entry_path in all_files {
559        // Skip files that PackageWriter regenerates
560        if entry_path == model_path
561            || entry_path == "_rels/.rels"
562            || entry_path == "[Content_Types].xml"
563        {
564            continue;
565        }
566
567        // Load .rels files to preserve relationships
568        if entry_path.ends_with(".rels") {
569            if let Ok(data) = archiver.read_entry(&entry_path) {
570                if let Ok(rels) = lib3mf_core::archive::opc::parse_relationships(&data) {
571                    model.existing_relationships.insert(entry_path, rels);
572                }
573            }
574            continue;
575        }
576
577        // Load other data as attachments
578        if let Ok(data) = archiver.read_entry(&entry_path) {
579            model.attachments.insert(entry_path, data);
580        }
581    }
582
583    let file = File::create(&output)
584        .map_err(|e| anyhow::anyhow!("Failed to create output file: {}", e))?;
585    model
586        .write(file)
587        .map_err(|e| anyhow::anyhow!("Failed to write 3MF: {}", e))?;
588
589    println!("Copied {:?} to {:?}", input, output);
590    Ok(())
591}
592
593fn open_archive(path: &PathBuf) -> anyhow::Result<ZipArchiver<File>> {
594    let file =
595        File::open(path).map_err(|e| anyhow::anyhow!("Failed to open file {:?}: {}", path, e))?;
596    ZipArchiver::new(file).map_err(|e| anyhow::anyhow!("Failed to open zip archive: {}", e))
597}
598
599fn build_file_tree(paths: &[String]) -> node::FileNode {
600    let mut root = node::FileNode::new_dir();
601    for path in paths {
602        let parts: Vec<&str> = path.split('/').collect();
603        root.insert(&parts);
604    }
605    root
606}
607
608fn print_tree(paths: &[String]) {
609    // Legacy tree printer
610    // Build a map of path components
611    let mut tree: BTreeMap<String, node::Node> = BTreeMap::new();
612
613    for path in paths {
614        let parts: Vec<&str> = path.split('/').collect();
615        let mut current_level = &mut tree;
616
617        for (i, part) in parts.iter().enumerate() {
618            let _is_file = i == parts.len() - 1;
619            let node = current_level
620                .entry(part.to_string())
621                .or_insert_with(node::Node::new);
622            current_level = &mut node.children;
623        }
624    }
625
626    node::print_nodes(&tree, "");
627}
628
629fn print_model_hierarchy(model: &lib3mf_core::model::Model) {
630    let mut tree: BTreeMap<String, node::Node> = BTreeMap::new();
631
632    for (i, item) in model.build.items.iter().enumerate() {
633        let (obj_name, obj_type) = model
634            .resources
635            .get_object(item.object_id)
636            .map(|obj| {
637                (
638                    obj.name
639                        .clone()
640                        .unwrap_or_else(|| format!("Object {}", item.object_id.0)),
641                    obj.object_type,
642                )
643            })
644            .unwrap_or_else(|| {
645                (
646                    format!("Object {}", item.object_id.0),
647                    lib3mf_core::model::ObjectType::Model,
648                )
649            });
650
651        let name = format!(
652            "Build Item {} [{}] (type: {}, ID: {})",
653            i + 1,
654            obj_name,
655            obj_type,
656            item.object_id.0
657        );
658        let node = tree.entry(name).or_insert_with(node::Node::new);
659
660        // Recurse into objects
661        add_object_to_tree(model, item.object_id, node);
662    }
663
664    node::print_nodes(&tree, "");
665}
666
667fn add_object_to_tree(
668    model: &lib3mf_core::model::Model,
669    id: lib3mf_core::model::ResourceId,
670    parent: &mut node::Node,
671) {
672    if let Some(obj) = model.resources.get_object(id) {
673        match &obj.geometry {
674            lib3mf_core::model::Geometry::Mesh(mesh) => {
675                let info = format!(
676                    "Mesh: {} vertices, {} triangles",
677                    mesh.vertices.len(),
678                    mesh.triangles.len()
679                );
680                parent.children.insert(info, node::Node::new());
681            }
682            lib3mf_core::model::Geometry::Components(comps) => {
683                for (i, comp) in comps.components.iter().enumerate() {
684                    let child_obj_name = model
685                        .resources
686                        .get_object(comp.object_id)
687                        .and_then(|obj| obj.name.clone())
688                        .unwrap_or_else(|| format!("Object {}", comp.object_id.0));
689
690                    let name = format!(
691                        "Component {} [{}] (ID: {})",
692                        i + 1,
693                        child_obj_name,
694                        comp.object_id.0
695                    );
696                    let node = parent.children.entry(name).or_insert_with(node::Node::new);
697                    add_object_to_tree(model, comp.object_id, node);
698                }
699            }
700            _ => {
701                parent
702                    .children
703                    .insert("Unknown Geometry".to_string(), node::Node::new());
704            }
705        }
706    }
707}
708
709fn print_model_hierarchy_resolved<A: ArchiveReader>(
710    resolver: &mut lib3mf_core::model::resolver::PartResolver<A>,
711) {
712    let mut tree: BTreeMap<String, node::Node> = BTreeMap::new();
713
714    let build_items = resolver.get_root_model().build.items.clone();
715
716    for (i, item) in build_items.iter().enumerate() {
717        let (obj_name, obj_id, obj_type) = {
718            let res = resolver
719                .resolve_object(item.object_id, None)
720                .unwrap_or(None);
721            match res {
722                Some((_model, obj)) => (
723                    obj.name
724                        .clone()
725                        .unwrap_or_else(|| format!("Object {}", obj.id.0)),
726                    obj.id,
727                    obj.object_type,
728                ),
729                None => (
730                    format!("Missing Object {}", item.object_id.0),
731                    item.object_id,
732                    lib3mf_core::model::ObjectType::Model,
733                ),
734            }
735        };
736
737        let name = format!(
738            "Build Item {} [{}] (type: {}, ID: {})",
739            i + 1,
740            obj_name,
741            obj_type,
742            obj_id.0
743        );
744        let node = tree.entry(name).or_insert_with(node::Node::new);
745
746        // Recurse into objects
747        add_object_to_tree_resolved(resolver, obj_id, None, node);
748    }
749
750    node::print_nodes(&tree, "");
751}
752
753fn add_object_to_tree_resolved<A: ArchiveReader>(
754    resolver: &mut lib3mf_core::model::resolver::PartResolver<A>,
755    id: lib3mf_core::model::ResourceId,
756    path: Option<&str>,
757    parent: &mut node::Node,
758) {
759    let components = {
760        let resolved = resolver.resolve_object(id, path).unwrap_or(None);
761        if let Some((_model, obj)) = resolved {
762            match &obj.geometry {
763                lib3mf_core::model::Geometry::Mesh(mesh) => {
764                    let info = format!(
765                        "Mesh: {} vertices, {} triangles",
766                        mesh.vertices.len(),
767                        mesh.triangles.len()
768                    );
769                    parent.children.insert(info, node::Node::new());
770                    None
771                }
772                lib3mf_core::model::Geometry::Components(comps) => Some(comps.components.clone()),
773                _ => {
774                    parent
775                        .children
776                        .insert("Unknown Geometry".to_string(), node::Node::new());
777                    None
778                }
779            }
780        } else {
781            None
782        }
783    };
784
785    if let Some(comps) = components {
786        for (i, comp) in comps.iter().enumerate() {
787            let next_path = comp.path.as_deref().or(path);
788            let (child_obj_name, child_obj_id) = {
789                let res = resolver
790                    .resolve_object(comp.object_id, next_path)
791                    .unwrap_or(None);
792                match res {
793                    Some((_model, obj)) => (
794                        obj.name
795                            .clone()
796                            .unwrap_or_else(|| format!("Object {}", obj.id.0)),
797                        obj.id,
798                    ),
799                    None => (
800                        format!("Missing Object {}", comp.object_id.0),
801                        comp.object_id,
802                    ),
803                }
804            };
805
806            let name = format!(
807                "Component {} [{}] (ID: {})",
808                i + 1,
809                child_obj_name,
810                child_obj_id.0
811            );
812            let node = parent.children.entry(name).or_insert_with(node::Node::new);
813            add_object_to_tree_resolved(resolver, child_obj_id, next_path, node);
814        }
815    }
816}
817
818mod node {
819    use serde::Serialize;
820    use std::collections::BTreeMap;
821
822    #[derive(Serialize)]
823    #[serde(untagged)]
824    pub enum FileNode {
825        File(Empty),
826        Dir(BTreeMap<String, FileNode>),
827    }
828
829    #[derive(Serialize)]
830    pub struct Empty {}
831
832    impl FileNode {
833        pub fn new_dir() -> Self {
834            FileNode::Dir(BTreeMap::new())
835        }
836
837        pub fn new_file() -> Self {
838            FileNode::File(Empty {})
839        }
840
841        pub fn insert(&mut self, path_parts: &[&str]) {
842            if let FileNode::Dir(children) = self {
843                if let Some((first, rest)) = path_parts.split_first() {
844                    let entry = children
845                        .entry(first.to_string())
846                        .or_insert_with(FileNode::new_dir);
847
848                    if rest.is_empty() {
849                        // It's a file
850                        if let FileNode::Dir(sub) = entry {
851                            if sub.is_empty() {
852                                *entry = FileNode::new_file();
853                            } else {
854                                // Conflict: Path is both a dir and a file?
855                                // Keep as dir for now or handle appropriately.
856                                // In 3MF/Zip, this shouldn't happen usually for exact paths.
857                            }
858                        }
859                    } else {
860                        // Recurse
861                        entry.insert(rest);
862                    }
863                }
864            }
865        }
866    }
867
868    // Helper for legacy Node struct compatibility if needed,
869    // or just reimplement internal printing logic.
870    #[derive(Serialize)] // Optional, mainly for internal use
871    pub struct Node {
872        pub children: BTreeMap<String, Node>,
873    }
874
875    impl Node {
876        pub fn new() -> Self {
877            Self {
878                children: BTreeMap::new(),
879            }
880        }
881    }
882
883    pub fn print_nodes(nodes: &BTreeMap<String, Node>, prefix: &str) {
884        let count = nodes.len();
885        for (i, (name, node)) in nodes.iter().enumerate() {
886            let is_last = i == count - 1;
887            let connector = if is_last { "└── " } else { "├── " };
888            println!("{}{}{}", prefix, connector, name);
889
890            let child_prefix = if is_last { "    " } else { "│   " };
891            let new_prefix = format!("{}{}", prefix, child_prefix);
892            print_nodes(&node.children, &new_prefix);
893        }
894    }
895}
896
897/// Convert between 3D formats (3MF, STL, OBJ).
898///
899/// Auto-detects formats based on file extensions and performs the appropriate conversion.
900///
901/// Supported conversions:
902/// - STL (binary) → 3MF
903/// - OBJ → 3MF
904/// - 3MF → STL (binary)
905/// - 3MF → OBJ
906///
907/// # Arguments
908///
909/// * `input` - Input file path
910/// * `output` - Output file path
911///
912/// # Errors
913///
914/// Returns an error if the format is unsupported or conversion fails.
915///
916/// # Example
917///
918/// ```no_run
919/// use lib3mf_cli::commands::convert;
920/// use std::path::PathBuf;
921///
922/// # fn main() -> anyhow::Result<()> {
923/// convert(PathBuf::from("mesh.stl"), PathBuf::from("model.3mf"))?;
924/// # Ok(())
925/// # }
926/// ```
927pub fn convert(input: PathBuf, output: PathBuf) -> anyhow::Result<()> {
928    let output_ext = output
929        .extension()
930        .and_then(|e| e.to_str())
931        .unwrap_or("")
932        .to_lowercase();
933
934    // Special handling for STL export from 3MF to support components
935    if output_ext == "stl" {
936        // We need to keep the archive open for resolving components
937        // Try opening as archive (zip)
938        let file_res = File::open(&input);
939
940        let should_use_resolver = if let Ok(mut f) = file_res {
941            let mut magic = [0u8; 4];
942            f.read_exact(&mut magic).is_ok() && &magic == b"PK\x03\x04"
943        } else {
944            false
945        };
946
947        if should_use_resolver {
948            let mut archiver = open_archive(&input)?;
949            let model_path = find_model_path(&mut archiver)
950                .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
951            let model_data = archiver
952                .read_entry(&model_path)
953                .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
954            let model = parse_model(std::io::Cursor::new(model_data))
955                .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
956
957            let resolver = lib3mf_core::model::resolver::PartResolver::new(&mut archiver, model);
958            let file = File::create(&output)
959                .map_err(|e| anyhow::anyhow!("Failed to create output file: {}", e))?;
960
961            // Access the root model via resolver for export
962            let root_model = resolver.get_root_model().clone(); // Clone to pass to export, or export takes ref
963
964            lib3mf_converters::stl::StlExporter::write_with_resolver(&root_model, resolver, file)
965                .map_err(|e| anyhow::anyhow!("Failed to export STL: {}", e))?;
966
967            println!("Converted {:?} to {:?}", input, output);
968            return Ok(());
969        }
970    }
971
972    // Fallback to legacy conversion (or non-archive)
973    // 1. Load Model
974    let model = load_model(&input)?;
975
976    // 2. Export Model
977    let file = File::create(&output)
978        .map_err(|e| anyhow::anyhow!("Failed to create output file: {}", e))?;
979
980    match output_ext.as_str() {
981        "3mf" => {
982            model
983                .write(file)
984                .map_err(|e| anyhow::anyhow!("Failed to write 3MF: {}", e))?;
985        }
986        "stl" => {
987            lib3mf_converters::stl::StlExporter::write(&model, file)
988                .map_err(|e| anyhow::anyhow!("Failed to export STL: {}", e))?;
989        }
990        "obj" => {
991            lib3mf_converters::obj::ObjExporter::write(&model, file)
992                .map_err(|e| anyhow::anyhow!("Failed to export OBJ: {}", e))?;
993        }
994        _ => return Err(anyhow::anyhow!("Unsupported output format: {}", output_ext)),
995    }
996
997    println!("Converted {:?} to {:?}", input, output);
998    Ok(())
999}
1000
1001/// Validate a 3MF file against the specification.
1002///
1003/// Performs semantic validation at the specified strictness level:
1004/// - `minimal`: Basic file integrity checks
1005/// - `standard`: Reference integrity and structure validation
1006/// - `strict`: Full spec compliance including unit consistency
1007/// - `paranoid`: Deep geometry analysis (manifoldness, self-intersection)
1008///
1009/// # Arguments
1010///
1011/// * `path` - Path to the 3MF file
1012/// * `level` - Validation level string (minimal, standard, strict, paranoid)
1013///
1014/// # Errors
1015///
1016/// Returns an error if validation fails (errors found) or the file cannot be parsed.
1017///
1018/// # Exit Code
1019///
1020/// Exits with code 1 if validation errors are found, 0 if passed.
1021pub fn validate(path: PathBuf, level: String) -> anyhow::Result<()> {
1022    use lib3mf_core::validation::{ValidationLevel, ValidationSeverity};
1023
1024    let level_enum = match level.to_lowercase().as_str() {
1025        "minimal" => ValidationLevel::Minimal,
1026        "standard" => ValidationLevel::Standard,
1027        "strict" => ValidationLevel::Strict,
1028        "paranoid" => ValidationLevel::Paranoid,
1029        _ => ValidationLevel::Standard,
1030    };
1031
1032    println!("Validating {:?} at {:?} level...", path, level_enum);
1033
1034    let model = load_model(&path)?;
1035
1036    // Run comprehensive validation
1037    let report = model.validate(level_enum);
1038
1039    let errors: Vec<_> = report
1040        .items
1041        .iter()
1042        .filter(|i| i.severity == ValidationSeverity::Error)
1043        .collect();
1044    let warnings: Vec<_> = report
1045        .items
1046        .iter()
1047        .filter(|i| i.severity == ValidationSeverity::Warning)
1048        .collect();
1049
1050    if !errors.is_empty() {
1051        println!("Validation Failed with {} error(s):", errors.len());
1052        for item in &errors {
1053            println!("  [ERROR {}] {}", item.code, item.message);
1054        }
1055        std::process::exit(1);
1056    } else if !warnings.is_empty() {
1057        println!("Validation Passed with {} warning(s):", warnings.len());
1058        for item in &warnings {
1059            println!("  [WARN {}] {}", item.code, item.message);
1060        }
1061    } else {
1062        println!("Validation Passed.");
1063    }
1064
1065    Ok(())
1066}
1067
1068/// Repair mesh geometry in a 3MF file.
1069///
1070/// Performs geometric processing to improve printability:
1071/// - Vertex stitching (merge vertices within epsilon tolerance)
1072/// - Degenerate triangle removal
1073/// - Duplicate triangle removal
1074/// - Orientation harmonization (consistent winding)
1075/// - Island removal (disconnected components)
1076/// - Hole filling (boundary loop triangulation)
1077///
1078/// # Arguments
1079///
1080/// * `input` - Input 3MF file path
1081/// * `output` - Output 3MF file path
1082/// * `epsilon` - Vertex merge tolerance for stitching
1083/// * `fixes` - List of repair types to perform
1084///
1085/// # Errors
1086///
1087/// Returns an error if parsing or writing fails.
1088pub fn repair(
1089    input: PathBuf,
1090    output: PathBuf,
1091    epsilon: f32,
1092    fixes: Vec<RepairType>,
1093) -> anyhow::Result<()> {
1094    use lib3mf_core::model::{Geometry, MeshRepair, RepairOptions};
1095
1096    println!("Repairing {:?} -> {:?}", input, output);
1097
1098    let mut archiver = open_archive(&input)?;
1099    let model_path = find_model_path(&mut archiver)
1100        .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
1101    let model_data = archiver
1102        .read_entry(&model_path)
1103        .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
1104    let mut model = parse_model(std::io::Cursor::new(model_data))
1105        .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
1106
1107    let mut options = RepairOptions {
1108        stitch_epsilon: epsilon,
1109        remove_degenerate: false,
1110        remove_duplicate_faces: false,
1111        harmonize_orientations: false,
1112        remove_islands: false,
1113        fill_holes: false,
1114    };
1115
1116    let has_all = fixes.contains(&RepairType::All);
1117    for fix in fixes {
1118        match fix {
1119            RepairType::Degenerate => options.remove_degenerate = true,
1120            RepairType::Duplicates => options.remove_duplicate_faces = true,
1121            RepairType::Harmonize => options.harmonize_orientations = true,
1122            RepairType::Islands => options.remove_islands = true,
1123            RepairType::Holes => options.fill_holes = true,
1124            RepairType::All => {
1125                options.remove_degenerate = true;
1126                options.remove_duplicate_faces = true;
1127                options.harmonize_orientations = true;
1128                options.remove_islands = true;
1129                options.fill_holes = true;
1130            }
1131        }
1132    }
1133
1134    if has_all {
1135        options.remove_degenerate = true;
1136        options.remove_duplicate_faces = true;
1137        options.harmonize_orientations = true;
1138        options.remove_islands = true;
1139        options.fill_holes = true;
1140    }
1141
1142    println!("Repair Options: {:?}", options);
1143
1144    let mut total_vertices_removed = 0;
1145    let mut total_triangles_removed = 0;
1146    let mut total_triangles_flipped = 0;
1147    let mut total_triangles_added = 0;
1148
1149    for object in model.resources.iter_objects_mut() {
1150        if let Geometry::Mesh(mesh) = &mut object.geometry {
1151            let stats = mesh.repair(options);
1152            if stats.vertices_removed > 0
1153                || stats.triangles_removed > 0
1154                || stats.triangles_flipped > 0
1155                || stats.triangles_added > 0
1156            {
1157                println!(
1158                    "Repaired Object {}: Removed {} vertices, {} triangles. Flipped {}. Added {}.",
1159                    object.id.0,
1160                    stats.vertices_removed,
1161                    stats.triangles_removed,
1162                    stats.triangles_flipped,
1163                    stats.triangles_added
1164                );
1165                total_vertices_removed += stats.vertices_removed;
1166                total_triangles_removed += stats.triangles_removed;
1167                total_triangles_flipped += stats.triangles_flipped;
1168                total_triangles_added += stats.triangles_added;
1169            }
1170        }
1171    }
1172
1173    println!("Total Repair Stats:");
1174    println!("  Vertices Removed:  {}", total_vertices_removed);
1175    println!("  Triangles Removed: {}", total_triangles_removed);
1176    println!("  Triangles Flipped: {}", total_triangles_flipped);
1177    println!("  Triangles Added:   {}", total_triangles_added);
1178
1179    // Write output
1180    let file = File::create(&output)
1181        .map_err(|e| anyhow::anyhow!("Failed to create output file: {}", e))?;
1182    model
1183        .write(file)
1184        .map_err(|e| anyhow::anyhow!("Failed to write 3MF: {}", e))?;
1185
1186    Ok(())
1187}
1188
1189/// Benchmark loading and parsing performance.
1190///
1191/// Measures time taken for ZIP archive opening, XML parsing, and statistics calculation.
1192/// Useful for performance profiling and identifying bottlenecks.
1193///
1194/// # Arguments
1195///
1196/// * `path` - Path to the 3MF file
1197///
1198/// # Errors
1199///
1200/// Returns an error if the file cannot be opened or parsed.
1201pub fn benchmark(path: PathBuf) -> anyhow::Result<()> {
1202    use std::time::Instant;
1203
1204    println!("Benchmarking {:?}...", path);
1205
1206    let start = Instant::now();
1207    let mut archiver = open_archive(&path)?;
1208    let t_zip = start.elapsed();
1209
1210    let start_parse = Instant::now();
1211    let model_path = find_model_path(&mut archiver)
1212        .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
1213    let model_data = archiver
1214        .read_entry(&model_path)
1215        .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
1216    let model = parse_model(std::io::Cursor::new(model_data))
1217        .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
1218    let t_parse = start_parse.elapsed();
1219
1220    let start_stats = Instant::now();
1221    let stats = model
1222        .compute_stats(&mut archiver)
1223        .map_err(|e| anyhow::anyhow!("Failed to compute stats: {}", e))?;
1224    let t_stats = start_stats.elapsed();
1225
1226    let total = start.elapsed();
1227
1228    println!("Results:");
1229    println!(
1230        "  System: {} ({} CPUs), SIMD: {}",
1231        stats.system_info.architecture,
1232        stats.system_info.num_cpus,
1233        stats.system_info.simd_features.join(", ")
1234    );
1235    println!("  Zip Open: {:?}", t_zip);
1236    println!("  XML Parse: {:?}", t_parse);
1237    println!("  Stats Calc: {:?}", t_stats);
1238    println!("  Total: {:?}", total);
1239    println!("  Triangles: {}", stats.geometry.triangle_count);
1240    println!(
1241        "  Area: {:.2}, Volume: {:.2}",
1242        stats.geometry.surface_area, stats.geometry.volume
1243    );
1244
1245    Ok(())
1246}
1247
1248/// Compare two 3MF files structurally.
1249///
1250/// Performs a detailed comparison detecting differences in metadata, resource counts,
1251/// and build items.
1252///
1253/// # Arguments
1254///
1255/// * `file1` - First 3MF file path
1256/// * `file2` - Second 3MF file path
1257/// * `format` - Output format ("text" or "json")
1258///
1259/// # Errors
1260///
1261/// Returns an error if either file cannot be parsed.
1262pub fn diff(file1: PathBuf, file2: PathBuf, format: &str) -> anyhow::Result<()> {
1263    println!("Comparing {:?} and {:?}...", file1, file2);
1264
1265    let model_a = load_model(&file1)?;
1266    let model_b = load_model(&file2)?;
1267
1268    let diff = lib3mf_core::utils::diff::compare_models(&model_a, &model_b);
1269
1270    if format == "json" {
1271        println!("{}", serde_json::to_string_pretty(&diff)?);
1272    } else if diff.is_empty() {
1273        println!("Models are identical.");
1274    } else {
1275        println!("Differences found:");
1276        if !diff.metadata_diffs.is_empty() {
1277            println!("  Metadata:");
1278            for d in &diff.metadata_diffs {
1279                println!("    - {:?}: {:?} -> {:?}", d.key, d.old_value, d.new_value);
1280            }
1281        }
1282        if !diff.resource_diffs.is_empty() {
1283            println!("  Resources:");
1284            for d in &diff.resource_diffs {
1285                match d {
1286                    lib3mf_core::utils::diff::ResourceDiff::Added { id, type_name } => {
1287                        println!("    + Added ID {}: {}", id, type_name)
1288                    }
1289                    lib3mf_core::utils::diff::ResourceDiff::Removed { id, type_name } => {
1290                        println!("    - Removed ID {}: {}", id, type_name)
1291                    }
1292                    lib3mf_core::utils::diff::ResourceDiff::Changed { id, details } => {
1293                        println!("    * Changed ID {}:", id);
1294                        for det in details {
1295                            println!("      . {}", det);
1296                        }
1297                    }
1298                }
1299            }
1300        }
1301        if !diff.build_diffs.is_empty() {
1302            println!("  Build Items:");
1303            for d in &diff.build_diffs {
1304                println!("    - {:?}", d);
1305            }
1306        }
1307    }
1308
1309    Ok(())
1310}
1311
1312fn load_model(path: &PathBuf) -> anyhow::Result<lib3mf_core::model::Model> {
1313    match open_model(path)? {
1314        ModelSource::Archive(_, model) => Ok(model),
1315        ModelSource::Raw(model) => Ok(model),
1316    }
1317}
1318
1319/// Sign a 3MF file using an RSA key.
1320///
1321/// **Status:** Not yet implemented. lib3mf-rs currently supports verifying existing
1322/// signatures but not creating new ones.
1323///
1324/// # Arguments
1325///
1326/// * `_input` - Input 3MF file path
1327/// * `_output` - Output 3MF file path
1328/// * `_key` - Path to PEM-encoded private key
1329/// * `_cert` - Path to PEM-encoded certificate
1330///
1331/// # Errors
1332///
1333/// Always returns an error indicating the feature is not implemented.
1334pub fn sign(
1335    _input: PathBuf,
1336    _output: PathBuf,
1337    _key: PathBuf,
1338    _cert: PathBuf,
1339) -> anyhow::Result<()> {
1340    anyhow::bail!(
1341        "Sign command not implemented: lib3mf-rs currently supports reading/verifying \
1342        signed 3MF files but does not support creating signatures.\n\n\
1343        Implementing signing requires:\n\
1344        - RSA signing with PEM private keys\n\
1345        - XML-DSIG structure creation and canonicalization\n\
1346        - OPC package modification with signature relationships\n\
1347        - X.509 certificate embedding\n\n\
1348        To create signed 3MF files, use the official 3MF SDK or other tools.\n\
1349        Verification of existing signatures works via: {} verify <file>",
1350        std::env::args()
1351            .next()
1352            .unwrap_or_else(|| "lib3mf-cli".to_string())
1353    );
1354}
1355
1356/// Verify digital signatures in a 3MF file.
1357///
1358/// Checks all digital signatures present in the 3MF package and reports their validity.
1359/// Requires the `crypto` feature to be enabled.
1360///
1361/// # Arguments
1362///
1363/// * `file` - Path to the 3MF file
1364///
1365/// # Errors
1366///
1367/// Returns an error if signature verification fails or if any signatures are invalid.
1368///
1369/// # Feature Gate
1370///
1371/// This function is only available when compiled with the `crypto` feature.
1372#[cfg(feature = "crypto")]
1373pub fn verify(file: PathBuf) -> anyhow::Result<()> {
1374    println!("Verifying signatures in {:?}...", file);
1375    let mut archiver = open_archive(&file)?;
1376
1377    // 1. Read Global Relationships to find signatures
1378    let rels_data = archiver.read_entry("_rels/.rels").unwrap_or_default();
1379    if rels_data.is_empty() {
1380        println!("No relationships found. File is not signed.");
1381        return Ok(());
1382    }
1383
1384    let rels = opc::parse_relationships(&rels_data)?;
1385    let sig_rels: Vec<_> = rels.iter().filter(|r|        r.rel_type == "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel/relationship/signature"
1386        || r.rel_type.ends_with("/signature") // Loose check
1387    ).collect();
1388
1389    if sig_rels.is_empty() {
1390        println!("No signature relationships found.");
1391        return Ok(());
1392    }
1393
1394    println!("Found {} signatures to verify.", sig_rels.len());
1395
1396    // Track verification results
1397    let mut all_valid = true;
1398    let mut signature_count = 0;
1399    let mut failed_signatures = Vec::new();
1400
1401    for rel in sig_rels {
1402        println!("Verifying signature: {}", rel.target);
1403        signature_count += 1;
1404        // Target is usually absolute path like "/Metadata/sig.xml"
1405        let target_path = rel.target.trim_start_matches('/');
1406
1407        let sig_xml_bytes = match archiver.read_entry(target_path) {
1408            Ok(b) => b,
1409            Err(e) => {
1410                println!("  [ERROR] Failed to read signature part: {}", e);
1411                all_valid = false;
1412                failed_signatures.push(rel.target.clone());
1413                continue;
1414            }
1415        };
1416
1417        // Parse Signature
1418        let sig_xml_str = String::from_utf8_lossy(&sig_xml_bytes);
1419        // We use Cursor wrapping String for parser
1420        let mut sig_parser = lib3mf_core::parser::xml_parser::XmlParser::new(std::io::Cursor::new(
1421            sig_xml_bytes.clone(),
1422        ));
1423        let signature = match lib3mf_core::parser::crypto_parser::parse_signature(&mut sig_parser) {
1424            Ok(s) => s,
1425            Err(e) => {
1426                println!("  [ERROR] Failed to parse signature XML: {}", e);
1427                all_valid = false;
1428                failed_signatures.push(rel.target.clone());
1429                continue;
1430            }
1431        };
1432
1433        // Canonicalize SignedInfo
1434        // We need the Bytes of SignedInfo.
1435        // Option 1: Re-read file and extract substring (risky if not formatted same).
1436        // Option 2: Use Canonicalizer on the original bytes to extract subtree.
1437        let signed_info_c14n = match lib3mf_core::utils::c14n::Canonicalizer::canonicalize_subtree(
1438            &sig_xml_str,
1439            "SignedInfo",
1440        ) {
1441            Ok(b) => b,
1442            Err(e) => {
1443                println!("  [ERROR] Failed to extract/canonicalize SignedInfo: {}", e);
1444                all_valid = false;
1445                failed_signatures.push(rel.target.clone());
1446                continue;
1447            }
1448        };
1449
1450        // Prepare Content Resolver
1451        // This closure allows the verifier to fetch the bytes of parts referenced by the signature.
1452        // We need to clone the archive reader or access it safely.
1453        // Archiver is mut... tricky with closure if capturing mut ref.
1454        // But we iterate sequentially. We can pass a closure that reads from a shared ref or re-opens?
1455        // Actually, we can just pre-read referenced parts? No, References are inside Signature.
1456        // Ideally, we pass a closure. But `archiver` is needed.
1457        // Simpler: Read all entries into a Map? No, memory.
1458        // We can use a ref cell or mutex for archiver?
1459        // Or better: `verify_signature_extended` takes a closure.
1460        // The closure can't mutate archiver easily if archiver requires mut.
1461        // `ZipArchiver::read_entry` takes `&mut self`.
1462        // We can close and re-open? Inefficient.
1463
1464        // Hack: Read all referenced parts needed by THIS signature before calling verify?
1465        // But verify_signature calls the resolver.
1466        // Let's implement a wrapper struct or use RefCell.
1467        // `archiver` is `ZipArchiver<File>`.
1468        // Let's defer resolver implementation by collecting references first?
1469        // `verify_signature` logic iterates references and calls resolver.
1470        // If we duplicate the "resolve" logic:
1471        // 1. Collect URIs from signature.
1472        // 2. Read all contents into a Map.
1473        // 3. Pass Map lookup to verifier.
1474
1475        let mut content_map = BTreeMap::new();
1476        for ref_item in &signature.signed_info.references {
1477            let uri = &ref_item.uri;
1478            if uri.is_empty() {
1479                continue;
1480            } // Implicit reference to something?
1481            let part_path = uri.trim_start_matches('/');
1482            match archiver.read_entry(part_path) {
1483                Ok(data) => {
1484                    content_map.insert(uri.clone(), data);
1485                }
1486                Err(e) => println!("  [WARNING] Could not read referenced part {}: {}", uri, e),
1487            }
1488        }
1489
1490        let resolver = |uri: &str| -> lib3mf_core::error::Result<Vec<u8>> {
1491            content_map.get(uri).cloned().ok_or_else(|| {
1492                lib3mf_core::error::Lib3mfError::Validation(format!("Content not found: {}", uri))
1493            })
1494        };
1495
1496        match lib3mf_core::crypto::verification::verify_signature_extended(
1497            &signature,
1498            resolver,
1499            &signed_info_c14n,
1500        ) {
1501            Ok(valid) => {
1502                if valid {
1503                    println!("  [PASS] Signature is VALID.");
1504                    // Check certificate trust if present
1505                    if let Some(mut ki) = signature.key_info {
1506                        if let Some(x509) = ki.x509_data.take() {
1507                            if let Some(_cert_str) = x509.certificate {
1508                                println!(
1509                                    "  [INFO] Signed by X.509 Certificate (Trust check pending)"
1510                                );
1511                                // TODO: Validate chain
1512                            }
1513                        } else {
1514                            println!("  [INFO] Signed by Raw Key (Self-signed equivalent)");
1515                        }
1516                    }
1517                } else {
1518                    println!("  [FAIL] Signature is INVALID (Verification returned false).");
1519                    all_valid = false;
1520                    failed_signatures.push(rel.target.clone());
1521                }
1522            }
1523            Err(e) => {
1524                println!("  [FAIL] Verification Error: {}", e);
1525                all_valid = false;
1526                failed_signatures.push(rel.target.clone());
1527            }
1528        }
1529    }
1530
1531    // Return error if any signatures failed
1532    if !all_valid {
1533        anyhow::bail!(
1534            "Signature verification failed for {} of {} signature(s): {:?}",
1535            failed_signatures.len(),
1536            signature_count,
1537            failed_signatures
1538        );
1539    }
1540
1541    println!(
1542        "\nAll {} signature(s) verified successfully.",
1543        signature_count
1544    );
1545    Ok(())
1546}
1547
1548/// Verify digital signatures (crypto feature disabled).
1549///
1550/// This is a stub function that returns an error when the `crypto` feature is not enabled.
1551///
1552/// # Errors
1553///
1554/// Always returns an error indicating the crypto feature is required.
1555#[cfg(not(feature = "crypto"))]
1556pub fn verify(_file: PathBuf) -> anyhow::Result<()> {
1557    anyhow::bail!(
1558        "Signature verification requires the 'crypto' feature to be enabled.\n\
1559        The CLI was built without cryptographic support."
1560    )
1561}
1562
1563/// Encrypt a 3MF file.
1564///
1565/// **Status:** Not yet implemented. lib3mf-rs currently supports parsing encrypted files
1566/// but not creating them.
1567///
1568/// # Arguments
1569///
1570/// * `_input` - Input 3MF file path
1571/// * `_output` - Output 3MF file path
1572/// * `_recipient` - Recipient certificate (PEM)
1573///
1574/// # Errors
1575///
1576/// Always returns an error indicating the feature is not implemented.
1577pub fn encrypt(_input: PathBuf, _output: PathBuf, _recipient: PathBuf) -> anyhow::Result<()> {
1578    anyhow::bail!(
1579        "Encrypt command not implemented: lib3mf-rs currently supports reading/parsing \
1580        encrypted 3MF files but does not support creating encrypted packages.\n\n\
1581        Implementing encryption requires:\n\
1582        - AES-256-GCM content encryption\n\
1583        - RSA-OAEP key wrapping for recipients\n\
1584        - KeyStore XML structure creation\n\
1585        - OPC package modification with encrypted content types\n\
1586        - Encrypted relationship handling\n\n\
1587        To create encrypted 3MF files, use the official 3MF SDK or other tools.\n\
1588        Decryption of existing encrypted files is also not yet implemented."
1589    );
1590}
1591
1592/// Decrypt a 3MF file.
1593///
1594/// **Status:** Not yet implemented. lib3mf-rs currently supports parsing encrypted files
1595/// but not decrypting them.
1596///
1597/// # Arguments
1598///
1599/// * `_input` - Input 3MF file path
1600/// * `_output` - Output 3MF file path
1601/// * `_key` - Private key (PEM)
1602///
1603/// # Errors
1604///
1605/// Always returns an error indicating the feature is not implemented.
1606pub fn decrypt(_input: PathBuf, _output: PathBuf, _key: PathBuf) -> anyhow::Result<()> {
1607    anyhow::bail!(
1608        "Decrypt command not implemented: lib3mf-rs currently supports reading/parsing \
1609        encrypted 3MF files but does not support decrypting content.\n\n\
1610        Implementing decryption requires:\n\
1611        - KeyStore parsing and key unwrapping\n\
1612        - RSA-OAEP private key operations\n\
1613        - AES-256-GCM content decryption\n\
1614        - OPC package reconstruction with decrypted parts\n\
1615        - Consumer authorization validation\n\n\
1616        To decrypt 3MF files, use the official 3MF SDK or other tools.\n\
1617        The library can parse KeyStore and encryption metadata from encrypted files."
1618    );
1619}