Skip to main content

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