use super::{
CONTENT_TYPES_PATH, MODEL_REL_TYPE, Package, RELS_PATH, TEXTURE_REL_TYPE, THUMBNAIL_REL_TYPE,
};
use crate::error::{Error, Result};
use quick_xml::Reader;
use quick_xml::events::Event;
use std::io::Read;
use urlencoding::decode;
use zip::ZipArchive;
const MAX_PREALLOC_BYTES: usize = 64 * 1024;
const MAX_FILE_CONTENT_BYTES: usize = 1024 * 1024 * 1024;
pub(super) fn open<R: Read + std::io::Seek>(reader: R, lenient: bool) -> Result<Package<R>> {
let archive = ZipArchive::new(reader)?;
let mut package = Package { archive, lenient };
validate_opc_structure(&mut package, lenient)?;
Ok(package)
}
fn validate_opc_structure<R: Read + std::io::Seek>(
package: &mut Package<R>,
lenient: bool,
) -> Result<()> {
if !has_file(package, CONTENT_TYPES_PATH) {
return Err(Error::invalid_format_context(
"OPC package structure",
&format!(
"Missing required file '{}'. \
This file defines content types for the package and is required by the OPC specification. \
The 3MF file may be corrupt or improperly formatted.",
CONTENT_TYPES_PATH
),
));
}
if !has_file(package, RELS_PATH) {
return Err(Error::invalid_format_context(
"OPC package structure",
&format!(
"Missing required file '{}'. \
This file defines package relationships and is required by the OPC specification. \
The 3MF file may be corrupt or improperly formatted.",
RELS_PATH
),
));
}
validate_content_types(package, lenient)?;
validate_model_relationship(package)?;
validate_all_relationships(package, lenient)?;
Ok(())
}
fn validate_content_types<R: Read + std::io::Seek>(
package: &mut Package<R>,
lenient: bool,
) -> Result<()> {
let content = get_file(package, CONTENT_TYPES_PATH)?;
let mut reader = Reader::from_str(&content);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut found_rels = false;
let mut found_model = false;
let mut default_extensions = std::collections::HashSet::new();
let mut override_parts = std::collections::HashSet::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) | Ok(Event::Start(ref e)) => {
let name = e.name();
let name_str = std::str::from_utf8(name.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
if name_str.ends_with("Default") {
let mut extension = None;
let mut content_type = None;
for attr in e.attributes() {
let attr = attr?;
let key = std::str::from_utf8(attr.key.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
let value = std::str::from_utf8(&attr.value)
.map_err(|e| Error::InvalidXml(e.to_string()))?;
match key {
"Extension" => extension = Some(value.to_string()),
"ContentType" => content_type = Some(value.to_string()),
_ => {}
}
}
if let (Some(ext), Some(ct)) = (extension, content_type) {
if !lenient && ext.is_empty() {
return Err(Error::InvalidFormat(
"Content type Default element cannot have empty Extension attribute".to_string()
));
}
if !lenient && !default_extensions.insert(ext.clone()) {
return Err(Error::InvalidFormat(format!(
"Duplicate Default content type mapping for extension '{}'",
ext
)));
}
if !lenient && ext.eq_ignore_ascii_case("png") && ct != "image/png" {
return Err(Error::InvalidFormat(format!(
"Invalid content type '{}' for PNG extension, must be 'image/png'",
ct
)));
}
if ext.eq_ignore_ascii_case("rels")
&& ct == "application/vnd.openxmlformats-package.relationships+xml"
{
found_rels = true;
}
if ct == "application/vnd.ms-package.3dmanufacturing-3dmodel+xml" {
if !ext.eq_ignore_ascii_case("model")
&& !ext.eq_ignore_ascii_case("part")
{
return Err(Error::InvalidFormat(format!(
"Content type '{}' must use Extension='model' or 'part', not Extension='{}'",
ct, ext
)));
}
found_model = true;
}
}
} else if name_str.ends_with("Override") {
let mut part_name = None;
let mut content_type = None;
for attr in e.attributes() {
let attr = attr?;
let key = std::str::from_utf8(attr.key.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
let value = std::str::from_utf8(&attr.value)
.map_err(|e| Error::InvalidXml(e.to_string()))?;
match key {
"PartName" => part_name = Some(value.to_string()),
"ContentType" => content_type = Some(value.to_string()),
_ => {}
}
}
if let Some(ref pn) = part_name
&& pn.is_empty()
{
return Err(Error::InvalidFormat(
"Content type Override element cannot have empty PartName attribute"
.to_string(),
));
}
if let (Some(pn), Some(ct)) = (part_name, content_type) {
if !lenient && !override_parts.insert(pn.clone()) {
return Err(Error::InvalidFormat(format!(
"Duplicate Override content type for part '{}'",
pn
)));
}
if ct == "application/vnd.ms-package.3dmanufacturing-3dmodel+xml" {
found_model = true;
}
}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(Error::Xml(e)),
_ => {}
}
buf.clear();
}
if !found_rels {
return Err(Error::invalid_format_context(
"Content Types validation",
"Missing required 'rels' extension definition in [Content_Types].xml. \
The Content Types file must define a Default element for the '.rels' extension. \
This is required by the OPC specification.",
));
}
if !found_model {
return Err(Error::invalid_format_context(
"Content Types validation",
"Missing required model content type definition in [Content_Types].xml. \
The file must define either a Default or Override element for the 3D model content type \
('application/vnd.ms-package.3dmanufacturing-3dmodel+xml'). \
Check that the model file has a proper content type declaration.",
));
}
Ok(())
}
fn validate_model_relationship<R: Read + std::io::Seek>(package: &mut Package<R>) -> Result<()> {
let model_path = discover_model_path(package)?;
if let Some((_dir, filename)) = model_path.rsplit_once('/') {
if filename.starts_with('.') {
return Err(Error::InvalidFormat(format!(
"Model filename '{}' starts with a dot (hidden file). \
The 3MF specification requires standard naming for model files without dot prefix.",
filename
)));
}
if filename.contains("3dmodel") {
if let Some(pos) = filename.find("3dmodel") {
let prefix = &filename[..pos];
if !prefix.is_empty() && !prefix.is_ascii() {
return Err(Error::InvalidFormat(format!(
"Model filename '{}' contains non-ASCII characters before '3dmodel'. \
The 3MF specification requires standard ASCII naming for model files.",
filename
)));
}
}
}
}
let file_exists = has_file(package, &model_path) || {
if let Ok(decoded) = decode(&model_path) {
let decoded_path = decoded.into_owned();
decoded_path != model_path && has_file(package, &decoded_path)
} else {
false
}
};
if !file_exists {
return Err(Error::InvalidFormat(format!(
"Model relationship points to non-existent file: {}",
model_path
)));
}
Ok(())
}
fn validate_all_relationships<R: Read + std::io::Seek>(
package: &mut Package<R>,
lenient: bool,
) -> Result<()> {
let mut rels_files = Vec::new();
for i in 0..package.archive.len() {
if let Ok(file) = package.archive.by_index(i) {
let name = file.name().to_string();
if name.ends_with(".rels") {
rels_files.push(name);
}
}
}
for rels_file in &rels_files {
if rels_file.contains("/_rels/") && rels_file != RELS_PATH {
let parts: Vec<&str> = rels_file.split("/_rels/").collect();
if parts.len() == 2 {
let dir = parts[0];
let rels_filename = parts[1];
if let Some(part_filename) = rels_filename.strip_suffix(".rels") {
let expected_part_path = if dir.is_empty() {
part_filename.to_string()
} else {
format!("{}/{}", dir, part_filename)
};
if !lenient && !has_file(package, &expected_part_path) {
return Err(Error::InvalidFormat(format!(
"Relationship file '{}' references part '{}' which does not exist in the package.\n\
Per OPC specification, part-specific relationship files must have names matching their associated parts.",
rels_file, expected_part_path
)));
}
}
}
}
let rels_content = get_file(package, rels_file)?;
let mut reader = Reader::from_str(&rels_content);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut relationship_ids = std::collections::HashSet::new();
let mut relationship_targets = std::collections::HashMap::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) | Ok(Event::Start(ref e)) => {
let name = e.name();
let name_str = std::str::from_utf8(name.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
if name_str.ends_with("Relationship") {
let mut target = None;
let mut rel_type = None;
let mut rel_id = None;
for attr in e.attributes() {
let attr = attr?;
let key = std::str::from_utf8(attr.key.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
let value = std::str::from_utf8(&attr.value)
.map_err(|e| Error::InvalidXml(e.to_string()))?;
match key {
"Target" => target = Some(value.to_string()),
"Type" => rel_type = Some(value.to_string()),
"Id" => rel_id = Some(value.to_string()),
_ => {}
}
}
if let Some(ref id) = rel_id {
if !lenient && !relationship_ids.insert(id.clone()) {
return Err(Error::InvalidFormat(format!(
"Duplicate relationship ID '{}' in '{}'",
id, rels_file
)));
}
if !lenient
&& rels_file == RELS_PATH
&& let Some(first_char) = id.chars().next()
&& first_char.is_ascii_digit()
{
return Err(Error::InvalidFormat(format!(
"Relationship ID '{}' in root .rels cannot start with a digit",
id
)));
}
} else {
return Err(Error::InvalidFormat(format!(
"Relationship missing required Id attribute in '{}'",
rels_file
)));
}
if let Some(ref rt) = rel_type {
if rels_file.contains("3dmodel.model.rels")
&& rt.contains("3dmodel")
&& rt != MODEL_REL_TYPE
{
return Err(Error::InvalidFormat(format!(
"Incorrect relationship Type '{}' in 3dmodel.model.rels",
rt
)));
}
if !lenient
&& rels_file == RELS_PATH
&& rt.contains("thumbnail")
&& rt != THUMBNAIL_REL_TYPE
{
return Err(Error::InvalidFormat(format!(
"Incorrect thumbnail relationship Type '{}' in root .rels",
rt
)));
}
if !lenient
&& rels_file.contains("3dmodel.model.rels")
&& let Some(ref t) = target
{
let target_lower = t.to_lowercase();
if (target_lower.ends_with(".png")
|| target_lower.ends_with(".jpeg")
|| target_lower.ends_with(".jpg"))
&& rt == MODEL_REL_TYPE
{
return Err(Error::InvalidFormat(format!(
"Incorrect relationship Type '{}' for texture file '{}' in 3dmodel.model.rels.\n\
Per 3MF Material Extension spec, texture files must use relationship type '{}'.",
rt, t, TEXTURE_REL_TYPE
)));
}
}
if !lenient && rt.contains('?') {
return Err(Error::InvalidFormat(format!(
"Relationship Type in '{}' cannot contain query string: {}",
rels_file, rt
)));
}
if !lenient && rt.contains('#') {
return Err(Error::InvalidFormat(format!(
"Relationship Type in '{}' cannot contain fragment identifier: {}",
rels_file, rt
)));
}
}
if let Some(t) = target {
if !lenient && let Some(ref rt) = rel_type {
let key = (t.clone(), rt.clone());
if relationship_targets
.insert(key, rel_id.clone().unwrap_or_default())
.is_some()
{
return Err(Error::InvalidFormat(format!(
"Duplicate relationship to same target '{}' with type '{}' in '{}'",
t, rt, rels_file
)));
}
}
validate_opc_part_name(&t)?;
let path_with_slash = if let Some(stripped) = t.strip_prefix('/') {
stripped.to_string()
} else {
t.clone()
};
let file_exists = if has_file(package, &path_with_slash) {
true
} else {
if let Ok(decoded) = decode(&path_with_slash) {
let decoded_path = decoded.into_owned();
if decoded_path != path_with_slash {
has_file(package, &decoded_path)
} else {
false
}
} else {
false
}
};
if !file_exists && !lenient {
return Err(Error::InvalidFormat(format!(
"Relationship in '{}' points to non-existent file: {}",
rels_file, path_with_slash
)));
}
}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(Error::Xml(e)),
_ => {}
}
buf.clear();
}
}
Ok(())
}
fn resolve_model_path<R: Read + std::io::Seek>(package: &mut Package<R>) -> Result<String> {
let model_path = discover_model_path(package)?;
if has_file(package, &model_path) {
Ok(model_path)
} else {
if let Ok(decoded) = decode(&model_path) {
let decoded_path = decoded.into_owned();
if decoded_path != model_path && has_file(package, &decoded_path) {
Ok(decoded_path)
} else {
Err(Error::MissingFile(model_path))
}
} else {
Err(Error::MissingFile(model_path))
}
}
}
pub(super) fn get_model<R: Read + std::io::Seek>(package: &mut Package<R>) -> Result<String> {
let path_to_use = resolve_model_path(package)?;
let mut file = package
.archive
.by_name(&path_to_use)
.map_err(|_| Error::MissingFile(path_to_use.clone()))?;
let capacity = (file.size() as usize).min(MAX_PREALLOC_BYTES);
let mut content = String::with_capacity(capacity);
(&mut file)
.take(MAX_FILE_CONTENT_BYTES as u64 + 1)
.read_to_string(&mut content)?;
if content.len() > MAX_FILE_CONTENT_BYTES {
return Err(Error::InvalidFormat(format!(
"File '{}' exceeds maximum allowed size of {} bytes",
path_to_use, MAX_FILE_CONTENT_BYTES
)));
}
Ok(content)
}
pub(super) fn get_model_reader<'a, R: Read + std::io::Seek>(
package: &'a mut Package<R>,
) -> Result<impl Read + 'a> {
let path_to_use = resolve_model_path(package)?;
let file = package
.archive
.by_name(&path_to_use)
.map_err(|_| Error::MissingFile(path_to_use.clone()))?;
Ok(file.take(MAX_FILE_CONTENT_BYTES as u64 + 1))
}
pub(super) fn get_file<R: Read + std::io::Seek>(
package: &mut Package<R>,
name: &str,
) -> Result<String> {
let mut file = package
.archive
.by_name(name)
.map_err(|_| Error::MissingFile(name.to_string()))?;
let capacity = (file.size() as usize).min(MAX_PREALLOC_BYTES);
let mut content = String::with_capacity(capacity);
(&mut file)
.take(MAX_FILE_CONTENT_BYTES as u64 + 1)
.read_to_string(&mut content)?;
if content.len() > MAX_FILE_CONTENT_BYTES {
return Err(Error::InvalidFormat(format!(
"File '{}' exceeds maximum allowed size of {} bytes",
name, MAX_FILE_CONTENT_BYTES
)));
}
Ok(content)
}
pub(super) fn has_file<R: Read + std::io::Seek>(package: &mut Package<R>, name: &str) -> bool {
package.archive.by_name(name).is_ok()
}
pub(super) fn len<R: Read + std::io::Seek>(package: &Package<R>) -> usize {
package.archive.len()
}
pub(super) fn is_empty<R: Read + std::io::Seek>(package: &Package<R>) -> bool {
package.archive.is_empty()
}
pub(super) fn file_names<R: Read + std::io::Seek>(package: &mut Package<R>) -> Vec<String> {
(0..package.archive.len())
.filter_map(|i| {
package
.archive
.by_index(i)
.ok()
.map(|f| f.name().to_string())
})
.collect()
}
pub(super) fn get_file_binary<R: Read + std::io::Seek>(
package: &mut Package<R>,
name: &str,
) -> Result<Vec<u8>> {
let mut file = package
.archive
.by_name(name)
.map_err(|_| Error::MissingFile(name.to_string()))?;
let capacity = (file.size() as usize).min(MAX_PREALLOC_BYTES);
let mut content = Vec::with_capacity(capacity);
(&mut file)
.take(MAX_FILE_CONTENT_BYTES as u64 + 1)
.read_to_end(&mut content)?;
if content.len() > MAX_FILE_CONTENT_BYTES {
return Err(Error::InvalidFormat(format!(
"File '{}' exceeds maximum allowed size of {} bytes",
name, MAX_FILE_CONTENT_BYTES
)));
}
Ok(content)
}
fn discover_model_path<R: Read + std::io::Seek>(package: &mut Package<R>) -> Result<String> {
let rels_content = get_file(package, RELS_PATH)?;
let mut reader = Reader::from_str(&rels_content);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) | Ok(Event::Start(ref e)) => {
let name = e.name();
let name_str = std::str::from_utf8(name.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
if name_str.ends_with("Relationship") {
let mut target = None;
let mut rel_type = None;
for attr in e.attributes() {
let attr = attr?;
let key = std::str::from_utf8(attr.key.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
let value = std::str::from_utf8(&attr.value)
.map_err(|e| Error::InvalidXml(e.to_string()))?;
match key {
"Target" => target = Some(value.to_string()),
"Type" => rel_type = Some(value.to_string()),
_ => {}
}
}
if let (Some(t), Some(rt)) = (target, rel_type)
&& rt == MODEL_REL_TYPE
{
let path = if let Some(stripped) = t.strip_prefix('/') {
stripped.to_string()
} else {
t
};
return Ok(path);
}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(Error::InvalidXml(e.to_string())),
_ => {}
}
buf.clear();
}
Err(Error::MissingFile(
"3D model relationship not found".to_string(),
))
}
fn validate_opc_part_name(part_name: &str) -> Result<()> {
if part_name.chars().any(|c| c.is_control()) {
return Err(Error::InvalidFormat(format!(
"Part name cannot contain control characters (newlines, tabs, etc.): {}",
part_name.escape_debug()
)));
}
if part_name.contains('#') {
return Err(Error::InvalidFormat(format!(
"Part name cannot contain fragment identifier: {}",
part_name
)));
}
if part_name.contains('?') {
return Err(Error::InvalidFormat(format!(
"Part name cannot contain query string: {}",
part_name
)));
}
let segments: Vec<&str> = part_name.split('/').collect();
for (idx, segment) in segments.iter().enumerate() {
if segment.is_empty() {
if idx == 0 && part_name.starts_with('/') {
continue;
}
return Err(Error::InvalidFormat(format!(
"Part name cannot contain empty path segments (consecutive slashes): {}",
part_name
)));
}
if *segment == "." || *segment == ".." {
return Err(Error::InvalidFormat(format!(
"Part name cannot contain '.' or '..' segments: {}",
part_name
)));
}
if segment.ends_with('.') {
return Err(Error::InvalidFormat(format!(
"Part name segments cannot end with '.': {}",
part_name
)));
}
}
Ok(())
}
#[allow(dead_code)]
fn get_content_type<R: Read + std::io::Seek>(
package: &mut Package<R>,
path: &str,
) -> Result<String> {
let content = get_file(package, CONTENT_TYPES_PATH)?;
let mut reader = Reader::from_str(&content);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
let path_normalized = normalize_path(path);
let extension = path.rsplit('.').next();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) | Ok(Event::Start(ref e)) => {
let name = e.name();
let name_str = std::str::from_utf8(name.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
if name_str.ends_with("Override") {
let mut part_name = None;
let mut content_type = None;
for attr in e.attributes() {
let attr = attr?;
let key = std::str::from_utf8(attr.key.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
let value = std::str::from_utf8(&attr.value)
.map_err(|e| Error::InvalidXml(e.to_string()))?;
match key {
"PartName" => part_name = Some(value.to_string()),
"ContentType" => content_type = Some(value.to_string()),
_ => {}
}
}
if let (Some(pn), Some(ct)) = (part_name, content_type) {
let pn_normalized = normalize_path(&pn);
if pn_normalized == path_normalized {
return Ok(ct);
}
}
}
else if name_str.ends_with("Default")
&& let Some(ext) = extension
{
let mut ext_attr = None;
let mut content_type = None;
for attr in e.attributes() {
let attr = attr?;
let key = std::str::from_utf8(attr.key.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
let value = std::str::from_utf8(&attr.value)
.map_err(|e| Error::InvalidXml(e.to_string()))?;
match key {
"Extension" => ext_attr = Some(value.to_string()),
"ContentType" => content_type = Some(value.to_string()),
_ => {}
}
}
if let (Some(e), Some(ct)) = (ext_attr, content_type)
&& e.eq_ignore_ascii_case(ext)
{
return Ok(ct);
}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(Error::Xml(e)),
_ => {}
}
buf.clear();
}
Err(Error::InvalidFormat(format!(
"No content type found for file: {}",
path
)))
}
fn normalize_path(path: &str) -> &str {
path.strip_prefix('/').unwrap_or(path)
}