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