lib3mf_core/archive/
model_locator.rs

1use crate::archive::{ArchiveReader, opc::parse_relationships};
2use crate::error::{Lib3mfError, Result};
3
4/// Locates the path of the 3D model file within the archive.
5pub fn find_model_path<R: ArchiveReader>(archive: &mut R) -> Result<String> {
6    // 1. Read _rels/.rels
7    if !archive.entry_exists("_rels/.rels") {
8        return Err(Lib3mfError::InvalidStructure(
9            "Missing _rels/.rels".to_string(),
10        ));
11    }
12
13    let rels_data = archive.read_entry("_rels/.rels")?;
14    let rels = parse_relationships(&rels_data)?;
15
16    // 2. Validate relationships and find the 3D Model
17    // 3MF Core Spec: http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel
18    let model_rel_type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel";
19    let thumbnail_rel_type =
20        "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail";
21    let print_ticket_rel_type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/printticket";
22
23    let mut model_path: Option<String> = None;
24    let mut model_count = 0;
25    let mut print_ticket_count = 0;
26
27    for rel in rels {
28        // Check for external relationships - these are not allowed in 3MF
29        if rel.target_mode.to_lowercase() == "external" {
30            return Err(Lib3mfError::Validation(format!(
31                "External relationships are not allowed in 3MF packages. Found external relationship with target '{}'",
32                rel.target
33            )));
34        }
35
36        if rel.rel_type == model_rel_type {
37            model_count += 1;
38            if model_count > 1 {
39                return Err(Lib3mfError::Validation(
40                    "Multiple 3D model part relationships found. Only one 3D model part is allowed per package".to_string(),
41                ));
42            }
43
44            // Target paths in OPC are often relative to the root if they start with /
45            // or relative to the relations file location.
46            // For root relationships, they are usually relative to root.
47            let path = rel.target.clone();
48            model_path = Some(if path.starts_with('/') {
49                path.trim_start_matches('/').to_string()
50            } else {
51                path
52            });
53        } else if rel.rel_type == thumbnail_rel_type {
54            // Validate that thumbnail file exists
55            let thumb_path = if rel.target.starts_with('/') {
56                rel.target.trim_start_matches('/').to_string()
57            } else {
58                rel.target.clone()
59            };
60            if !archive.entry_exists(&thumb_path) {
61                return Err(Lib3mfError::Validation(format!(
62                    "Thumbnail file '{}' referenced in relationships does not exist in package",
63                    thumb_path
64                )));
65            }
66        } else if rel.rel_type == print_ticket_rel_type {
67            print_ticket_count += 1;
68            if print_ticket_count > 1 {
69                return Err(Lib3mfError::Validation(
70                    "Multiple print ticket relationships found. Only one print ticket is allowed per package".to_string(),
71                ));
72            }
73        }
74    }
75
76    model_path.ok_or_else(|| Lib3mfError::Validation("No 3D model relationship found".to_string()))
77}