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