mod content_types;
mod reader;
mod relationships;
mod thumbnail;
mod writer;
use crate::error::Result;
use std::io::Read;
use zip::ZipArchive;
pub use writer::{create_package, create_package_with_thumbnail};
pub const MODEL_PATH: &str = "3D/3dmodel.model";
pub const MODEL_PATH_ALT: &str = "/3D/3dmodel.model";
pub const CONTENT_TYPES_PATH: &str = "[Content_Types].xml";
pub const RELS_PATH: &str = "_rels/.rels";
pub const MODEL_RELS_PATH: &str = "3D/_rels/3dmodel.model.rels";
pub const MODEL_REL_TYPE: &str = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel";
pub const THUMBNAIL_REL_TYPE: &str =
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail";
pub const KEYSTORE_REL_TYPE_2019_04: &str =
"http://schemas.microsoft.com/3dmanufacturing/2019/04/keystore";
pub const KEYSTORE_REL_TYPE_2019_07: &str =
"http://schemas.microsoft.com/3dmanufacturing/2019/07/keystore";
pub const ENCRYPTEDFILE_REL_TYPE: &str =
"http://schemas.openxmlformats.org/package/2006/relationships/encryptedfile";
pub const TEXTURE_REL_TYPE: &str = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dtexture";
pub struct Package<R: Read> {
archive: ZipArchive<R>,
lenient: bool,
}
impl<R: Read + std::io::Seek> Package<R> {
pub fn open(reader: R) -> Result<Self> {
Self::open_lenient(reader, false)
}
pub fn open_lenient(reader: R, lenient: bool) -> Result<Self> {
reader::open(reader, lenient)
}
pub fn get_model(&mut self) -> Result<String> {
reader::get_model(self)
}
pub fn get_model_reader(&mut self) -> Result<impl Read + '_> {
reader::get_model_reader(self)
}
pub fn get_file(&mut self, name: &str) -> Result<String> {
reader::get_file(self, name)
}
pub fn has_file(&mut self, name: &str) -> bool {
reader::has_file(self, name)
}
pub fn len(&self) -> usize {
reader::len(self)
}
pub fn is_empty(&self) -> bool {
reader::is_empty(self)
}
pub fn file_names(&mut self) -> Vec<String> {
reader::file_names(self)
}
pub fn get_file_binary(&mut self, name: &str) -> Result<Vec<u8>> {
reader::get_file_binary(self, name)
}
pub fn get_thumbnail_metadata(&mut self) -> Result<Option<crate::model::Thumbnail>> {
thumbnail::get_thumbnail_metadata(self, self.lenient)
}
pub fn validate_no_model_level_thumbnails(&mut self) -> Result<()> {
thumbnail::validate_no_model_level_thumbnails(self, self.lenient)
}
pub fn discover_keystore_path(&mut self) -> Result<Option<String>> {
relationships::discover_keystore_path(self)
}
pub fn has_relationship_to_target(
&mut self,
target_path: &str,
relationship_type: &str,
source_file: Option<&str>,
) -> Result<bool> {
relationships::has_relationship_to_target(self, target_path, relationship_type, source_file)
}
pub fn validate_keystore_relationship(&mut self, keystore_path: &str) -> Result<()> {
relationships::validate_keystore_relationship(self, keystore_path)
}
pub fn validate_keystore_content_type(&mut self, keystore_path: &str) -> Result<()> {
content_types::validate_keystore_content_type(self, keystore_path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use std::io::Read;
use std::io::Write;
use zip::ZipWriter;
use zip::write::SimpleFileOptions;
fn make_zip(files: &[(&str, &[u8])]) -> Cursor<Vec<u8>> {
let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
let options = SimpleFileOptions::default();
for (name, data) in files {
zip.start_file(*name, options).unwrap();
zip.write_all(data).unwrap();
}
zip.finish().unwrap()
}
const MINIMAL_CONTENT_TYPES: &[u8] = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
</Types>";
const MINIMAL_RELS: &[u8] = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
</Relationships>";
const MINIMAL_MODEL: &[u8] = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<model unit=\"millimeter\" xml:lang=\"en-US\" \
xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\">\
<resources/><build/></model>";
fn minimal_3mf() -> Cursor<Vec<u8>> {
make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", MINIMAL_RELS),
("3D/3dmodel.model", MINIMAL_MODEL),
])
}
fn minimal_3mf_with_thumbnail() -> Cursor<Vec<u8>> {
let content_types = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Default Extension=\"png\" ContentType=\"image/png\"/>\
</Types>";
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/Metadata/thumbnail.png\" Id=\"rel1\" Type=\"http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail\"/>\
</Relationships>";
let png: &[u8] = &[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90,
0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xE2, 0x21,
0xBC, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, ];
make_zip(&[
("[Content_Types].xml", content_types),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
("Metadata/thumbnail.png", png),
])
}
fn minimal_3mf_with_keystore() -> Cursor<Vec<u8>> {
let content_types = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Default Extension=\"xml\" ContentType=\"application/vnd.ms-package.3dmanufacturing-keystore+xml\"/>\
</Types>";
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/Metadata/keystore.xml\" Id=\"rel1\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2019/07/keystore\"/>\
</Relationships>";
make_zip(&[
("[Content_Types].xml", content_types),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
(
"Metadata/keystore.xml",
b"<?xml version=\"1.0\"?><keystore/>",
),
])
}
#[test]
fn test_package_get_model() {
let mut pkg = Package::open(minimal_3mf()).unwrap();
let model = pkg.get_model().unwrap();
assert!(
model.contains("<model"),
"get_model should return model XML"
);
}
#[test]
fn test_package_get_model_reader() {
let mut pkg = Package::open(minimal_3mf()).unwrap();
let mut reader = pkg.get_model_reader().unwrap();
let mut content = String::new();
reader.read_to_string(&mut content).unwrap();
assert!(
content.contains("<model"),
"get_model_reader should stream model XML"
);
}
#[test]
fn test_package_get_file() {
let mut pkg = Package::open(minimal_3mf()).unwrap();
let content = pkg.get_file(RELS_PATH).unwrap();
assert!(
content.contains("Relationships"),
"get_file should return rels XML"
);
}
#[test]
fn test_package_has_file_existing_and_missing() {
let mut pkg = Package::open(minimal_3mf()).unwrap();
assert!(
pkg.has_file(MODEL_PATH),
"has_file should return true for existing file"
);
assert!(
!pkg.has_file("nonexistent.bin"),
"has_file should return false for missing file"
);
}
#[test]
fn test_package_len_and_is_empty() {
let pkg = Package::open(minimal_3mf()).unwrap();
assert_eq!(pkg.len(), 3, "minimal 3MF should have 3 files");
assert!(!pkg.is_empty(), "non-empty package should not be is_empty");
}
#[test]
fn test_package_file_names() {
let mut pkg = Package::open(minimal_3mf()).unwrap();
let names = pkg.file_names();
assert_eq!(names.len(), 3);
assert!(names.contains(&CONTENT_TYPES_PATH.to_string()));
assert!(names.contains(&RELS_PATH.to_string()));
assert!(names.contains(&MODEL_PATH.to_string()));
}
#[test]
fn test_package_get_file_binary() {
let mut pkg = Package::open(minimal_3mf()).unwrap();
let data = pkg.get_file_binary(MODEL_PATH).unwrap();
assert!(
!data.is_empty(),
"get_file_binary should return non-empty data"
);
assert_eq!(&data[..5], b"<?xml");
}
#[test]
fn test_package_get_file_missing_returns_error() {
let mut pkg = Package::open(minimal_3mf()).unwrap();
assert!(pkg.get_file("does_not_exist.xml").is_err());
assert!(pkg.get_file_binary("does_not_exist.bin").is_err());
}
#[test]
fn test_open_missing_rels_file() {
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(
result.is_err(),
"Package without _rels/.rels should fail to open"
);
let err = result.err().unwrap().to_string();
assert!(
err.contains("_rels/.rels") || err.contains("rels"),
"Error should mention missing rels file, got: {err}"
);
}
#[test]
fn test_content_types_missing_rels_extension() {
let ct = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
</Types>";
let cursor = make_zip(&[
("[Content_Types].xml", ct),
("_rels/.rels", MINIMAL_RELS),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("rels"),
"Error should mention missing rels extension, got: {err}"
);
}
#[test]
fn test_content_types_missing_model_type() {
let ct = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
</Types>";
let cursor = make_zip(&[
("[Content_Types].xml", ct),
("_rels/.rels", MINIMAL_RELS),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("model"),
"Error should mention missing model content type, got: {err}"
);
}
#[test]
fn test_content_types_empty_extension() {
let ct = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
</Types>";
let cursor = make_zip(&[
("[Content_Types].xml", ct),
("_rels/.rels", MINIMAL_RELS),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("empty"),
"Error should mention empty Extension, got: {err}"
);
}
#[test]
fn test_content_types_duplicate_extension() {
let ct = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
</Types>";
let cursor = make_zip(&[
("[Content_Types].xml", ct),
("_rels/.rels", MINIMAL_RELS),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("Duplicate"),
"Error should mention duplicate extension, got: {err}"
);
}
#[test]
fn test_content_types_invalid_png_content_type() {
let ct = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Default Extension=\"png\" ContentType=\"image/jpeg\"/>\
</Types>";
let cursor = make_zip(&[
("[Content_Types].xml", ct),
("_rels/.rels", MINIMAL_RELS),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("image/png"),
"Error should mention correct PNG content type, got: {err}"
);
}
#[test]
fn test_content_types_wrong_model_extension() {
let ct = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"xyz\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
</Types>";
let cursor = make_zip(&[
("[Content_Types].xml", ct),
("_rels/.rels", MINIMAL_RELS),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("Extension"),
"Error should mention Extension requirement, got: {err}"
);
}
#[test]
fn test_content_types_model_via_override_succeeds() {
let ct = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Override PartName=\"/3D/3dmodel.model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
</Types>";
let cursor = make_zip(&[
("[Content_Types].xml", ct),
("_rels/.rels", MINIMAL_RELS),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
assert!(
Package::open(cursor).is_ok(),
"Model content type via Override should be accepted"
);
}
#[test]
fn test_content_types_empty_partname() {
let ct = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Override PartName=\"\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
</Types>";
let cursor = make_zip(&[
("[Content_Types].xml", ct),
("_rels/.rels", MINIMAL_RELS),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("PartName"),
"Error should mention empty PartName, got: {err}"
);
}
#[test]
fn test_content_types_duplicate_override() {
let ct = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Override PartName=\"/3D/3dmodel.model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Override PartName=\"/3D/3dmodel.model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
</Types>";
let cursor = make_zip(&[
("[Content_Types].xml", ct),
("_rels/.rels", MINIMAL_RELS),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("Duplicate"),
"Error should mention duplicate Override, got: {err}"
);
}
#[test]
fn test_model_filename_dot_prefix() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/.3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/.3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("dot"),
"Error should mention dot-prefix filename, got: {err}"
);
}
#[test]
fn test_model_filename_non_ascii_prefix() {
let rels = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/\u{00C6}3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
</Relationships>";
let model_name = "3D/\u{00C6}3dmodel.model";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels.as_bytes()),
(model_name, MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("non-ASCII"),
"Error should mention non-ASCII prefix, got: {err}"
);
}
#[test]
fn test_model_file_not_found_in_zip() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/missing.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("missing") || err.contains("non-existent") || err.contains("exist"),
"Error should indicate file not found, got: {err}"
);
}
#[test]
fn test_duplicate_relationship_ids() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("Duplicate") || err.contains("duplicate"),
"Error should mention duplicate ID, got: {err}"
);
}
#[test]
fn test_relationship_id_starts_with_digit_in_root_rels() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"1invalid\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("digit"),
"Error should mention ID starting with digit, got: {err}"
);
}
#[test]
fn test_relationship_missing_id_attribute() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/3D/3dmodel.model\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("Id"),
"Error should mention missing Id attribute, got: {err}"
);
}
#[test]
fn test_wrong_relationship_type_for_texture_file() {
let ct = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Default Extension=\"png\" ContentType=\"image/png\"/>\
</Types>";
let model_rels =
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/texture.png\" Id=\"tex0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", ct),
("_rels/.rels", MINIMAL_RELS),
("3D/3dmodel.model", MINIMAL_MODEL),
("3D/_rels/3dmodel.model.rels", model_rels),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("texture") || err.contains("3dtexture"),
"Error should mention texture relationship type, got: {err}"
);
}
#[test]
fn test_relationship_type_with_query_string() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel1\" Type=\"http://example.com/type?query=1\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("query"),
"Error should mention query string, got: {err}"
);
}
#[test]
fn test_relationship_type_with_fragment() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel1\" Type=\"http://example.com/type#frag\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("fragment"),
"Error should mention fragment identifier, got: {err}"
);
}
#[test]
fn test_duplicate_relationship_targets() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel1\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("Duplicate") || err.contains("duplicate"),
"Error should mention duplicate target, got: {err}"
);
}
#[test]
fn test_part_specific_rels_without_associated_part() {
let orphan_rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", MINIMAL_RELS),
("3D/3dmodel.model", MINIMAL_MODEL),
("3D/_rels/orphan.model.rels", orphan_rels),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("orphan") || err.contains("exist"),
"Error should mention missing associated part, got: {err}"
);
}
#[test]
fn test_invalid_part_name_with_hash() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/3D/bad#part.model\" Id=\"rel1\" Type=\"http://example.com/other\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("fragment") || err.contains('#'),
"Error should mention fragment in part name, got: {err}"
);
}
#[test]
fn test_invalid_part_name_with_question_mark() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/3D/bad?part.model\" Id=\"rel1\" Type=\"http://example.com/other\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("query") || err.contains('?'),
"Error should mention query string in part name, got: {err}"
);
}
#[test]
fn test_invalid_part_name_with_dotdot_segment() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/3D/../etc/passwd\" Id=\"rel1\" Type=\"http://example.com/other\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains(".."),
"Error should mention '..' segment, got: {err}"
);
}
#[test]
fn test_invalid_part_name_with_single_dot_segment() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/3D/./other.model\" Id=\"rel1\" Type=\"http://example.com/other\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("'.'"),
"Error should mention '.' segment, got: {err}"
);
}
#[test]
fn test_invalid_part_name_segment_ends_with_dot() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/3D./other.model\" Id=\"rel1\" Type=\"http://example.com/other\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("'.'") || err.contains("end"),
"Error should mention segment ending with dot, got: {err}"
);
}
#[test]
fn test_invalid_part_name_empty_path_segment() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/3D//other.model\" Id=\"rel1\" Type=\"http://example.com/other\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let result = Package::open(cursor);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("empty") || err.contains("segment"),
"Error should mention empty path segment, got: {err}"
);
}
#[test]
fn test_get_thumbnail_metadata_returns_none_when_no_thumbnail() {
let mut pkg = Package::open(minimal_3mf()).unwrap();
let result = pkg.get_thumbnail_metadata().unwrap();
assert!(
result.is_none(),
"Package without thumbnail should return None"
);
}
#[test]
fn test_get_thumbnail_metadata_png() {
let mut pkg = Package::open(minimal_3mf_with_thumbnail()).unwrap();
let thumb = pkg.get_thumbnail_metadata().unwrap();
assert!(thumb.is_some(), "Package with thumbnail should return Some");
let thumb = thumb.unwrap();
assert!(
thumb.path.contains("thumbnail"),
"Thumbnail path should contain 'thumbnail'"
);
assert_eq!(&thumb.content_type, "image/png");
}
#[test]
fn test_thumbnail_cmyk_jpeg_rejected() {
let cmyk_jpeg: Vec<u8> = vec![
0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01, 0x00, 0x01, 0x04, ];
let ct = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Default Extension=\"jpeg\" ContentType=\"image/jpeg\"/>\
</Types>";
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/Metadata/thumbnail.jpeg\" Id=\"rel1\" Type=\"http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", ct),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
("Metadata/thumbnail.jpeg", &cmyk_jpeg),
]);
let mut pkg = Package::open(cursor).expect("Package with CMYK JPEG should open");
let result = pkg.get_thumbnail_metadata();
assert!(result.is_err(), "CMYK JPEG thumbnail should be rejected");
let err = result.err().unwrap().to_string();
assert!(
err.contains("CMYK"),
"Error should mention CMYK, got: {err}"
);
}
#[test]
fn test_validate_no_model_level_thumbnail_with_package_thumbnail_ok() {
let ct = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Default Extension=\"png\" ContentType=\"image/png\"/>\
</Types>";
let pkg_rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/Metadata/thumbnail.png\" Id=\"rel1\" Type=\"http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail\"/>\
</Relationships>";
let model_rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/Metadata/thumbnail.png\" Id=\"mrel0\" Type=\"http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail\"/>\
</Relationships>";
let png = &[0x89u8, 0x50, 0x4E, 0x47]; let cursor = make_zip(&[
("[Content_Types].xml", ct),
("_rels/.rels", pkg_rels),
("3D/3dmodel.model", MINIMAL_MODEL),
("3D/_rels/3dmodel.model.rels", model_rels),
("Metadata/thumbnail.png", png),
]);
let mut pkg = Package::open(cursor).expect("Package should open");
assert!(
pkg.validate_no_model_level_thumbnails().is_ok(),
"Model-level thumbnail is allowed when package-level thumbnail also exists"
);
}
#[test]
fn test_validate_model_level_thumbnail_without_package_level_fails() {
let ct = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Default Extension=\"png\" ContentType=\"image/png\"/>\
</Types>";
let pkg_rels = MINIMAL_RELS;
let model_rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/Metadata/thumbnail.png\" Id=\"mrel0\" Type=\"http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail\"/>\
</Relationships>";
let png = &[0x89u8, 0x50, 0x4E, 0x47];
let cursor = make_zip(&[
("[Content_Types].xml", ct),
("_rels/.rels", pkg_rels),
("3D/3dmodel.model", MINIMAL_MODEL),
("3D/_rels/3dmodel.model.rels", model_rels),
("Metadata/thumbnail.png", png),
]);
let mut pkg = Package::open(cursor).expect("Package should open");
let result = pkg.validate_no_model_level_thumbnails();
assert!(
result.is_err(),
"Model-level thumbnail without package-level thumbnail should fail"
);
}
#[test]
fn test_discover_keystore_path_returns_none_when_absent() {
let mut pkg = Package::open(minimal_3mf()).unwrap();
let path = pkg.discover_keystore_path().unwrap();
assert!(
path.is_none(),
"No keystore relationship should return None"
);
}
#[test]
fn test_discover_keystore_path_returns_path_when_present() {
let mut pkg = Package::open(minimal_3mf_with_keystore()).unwrap();
let path = pkg.discover_keystore_path().unwrap();
assert_eq!(path, Some("Metadata/keystore.xml".to_string()));
}
#[test]
fn test_has_relationship_to_target_found() {
let mut pkg = Package::open(minimal_3mf_with_thumbnail()).unwrap();
let found = pkg
.has_relationship_to_target(
"Metadata/thumbnail.png",
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail",
None,
)
.unwrap();
assert!(found, "Should find the thumbnail relationship");
}
#[test]
fn test_has_relationship_to_target_not_found() {
let mut pkg = Package::open(minimal_3mf()).unwrap();
let found = pkg
.has_relationship_to_target(
"Metadata/thumbnail.png",
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail",
None,
)
.unwrap();
assert!(
!found,
"Should not find thumbnail relationship in package without thumbnail"
);
}
#[test]
fn test_has_relationship_to_target_with_source_file_not_found() {
let mut pkg = Package::open(minimal_3mf()).unwrap();
let found = pkg
.has_relationship_to_target(
"3D/3dmodel.model",
"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel",
Some("3D/3dmodel.model"),
)
.unwrap();
assert!(
!found,
"Should return false when the associated .rels file does not exist"
);
}
#[test]
fn test_validate_keystore_relationship_fails_when_absent() {
let mut pkg = Package::open(minimal_3mf()).unwrap();
let result = pkg.validate_keystore_relationship("Metadata/keystore.xml");
assert!(
result.is_err(),
"Should fail when no keystore relationship exists"
);
}
#[test]
fn test_validate_keystore_relationship_succeeds_when_present() {
let mut pkg = Package::open(minimal_3mf_with_keystore()).unwrap();
assert!(
pkg.validate_keystore_relationship("Metadata/keystore.xml")
.is_ok(),
"Should succeed when keystore relationship is present"
);
}
#[test]
fn test_validate_keystore_content_type_via_default_extension() {
let mut pkg = Package::open(minimal_3mf_with_keystore()).unwrap();
assert!(
pkg.validate_keystore_content_type("Metadata/keystore.xml")
.is_ok(),
"Should accept keystore content type declared via Default Extension='xml'"
);
}
#[test]
fn test_validate_keystore_content_type_via_override() {
let ct = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Override PartName=\"/Metadata/keystore.xml\" ContentType=\"application/vnd.ms-package.3dmanufacturing-keystore+xml\"/>\
</Types>";
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/Metadata/keystore.xml\" Id=\"rel1\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2019/07/keystore\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", ct),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
(
"Metadata/keystore.xml",
b"<?xml version=\"1.0\"?><keystore/>",
),
]);
let mut pkg = Package::open(cursor).unwrap();
assert!(
pkg.validate_keystore_content_type("Metadata/keystore.xml")
.is_ok(),
"Should accept keystore content type declared via Override PartName"
);
}
#[test]
fn test_validate_keystore_content_type_fails_when_absent() {
let mut pkg = Package::open(minimal_3mf()).unwrap();
let result = pkg.validate_keystore_content_type("Metadata/keystore.xml");
assert!(
result.is_err(),
"Should fail when no keystore content type exists"
);
}
#[test]
fn test_create_package_with_thumbnail_no_thumbnail_data() {
let model_xml = std::str::from_utf8(MINIMAL_MODEL).unwrap();
let buf =
create_package_with_thumbnail(Cursor::new(Vec::new()), model_xml, None, None).unwrap();
let mut pkg = Package::open(buf).unwrap();
assert!(pkg.get_model().is_ok());
}
#[test]
fn test_create_package_with_jpeg_thumbnail() {
let model_xml = std::str::from_utf8(MINIMAL_MODEL).unwrap();
let thumb_data = &[0xFFu8, 0xD8, 0xFF, 0xE0]; let buf = create_package_with_thumbnail(
Cursor::new(Vec::new()),
model_xml,
Some(thumb_data),
Some("image/jpeg"),
)
.unwrap();
assert!(Package::open(buf).is_ok());
}
#[test]
fn test_package_constants() {
assert_eq!(MODEL_PATH, "3D/3dmodel.model");
assert_eq!(CONTENT_TYPES_PATH, "[Content_Types].xml");
}
#[test]
fn test_package_from_empty_zip() {
let buffer = Vec::new();
let cursor = Cursor::new(buffer);
let zip = ZipWriter::new(cursor);
let cursor = zip.finish().unwrap();
let result = Package::open(cursor);
assert!(
result.is_err(),
"Expected package validation to fail for empty ZIP"
);
}
#[test]
fn test_percent_encoded_part_names() {
let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
let options = SimpleFileOptions::default();
zip.start_file("[Content_Types].xml", options).unwrap();
zip.write_all(
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>
</Types>",
)
.unwrap();
zip.start_file("_rels/.rels", options).unwrap();
zip.write_all(
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">
<Relationship Target=\"/2D/test%C3%86file.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>
</Relationships>",
)
.unwrap();
zip.start_file("2D/testÆfile.model", options).unwrap();
zip.write_all(
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<model unit=\"millimeter\" xml:lang=\"en-US\" xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\">
<resources>
<object id=\"1\" type=\"model\">
<mesh>
<vertices>
<vertex x=\"0\" y=\"0\" z=\"0\"/>
<vertex x=\"100\" y=\"0\" z=\"0\"/>
<vertex x=\"0\" y=\"100\" z=\"0\"/>
</vertices>
<triangles>
<triangle v1=\"0\" v2=\"1\" v3=\"2\"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid=\"1\"/>
</build>
</model>",
)
.unwrap();
let cursor = zip.finish().unwrap();
let result = Package::open(cursor);
assert!(
result.is_ok(),
"Package with percent-encoded part names should open successfully"
);
}
#[test]
fn test_utf8_in_xml_accepted_for_compatibility() {
let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
let options = SimpleFileOptions::default();
zip.start_file("[Content_Types].xml", options).unwrap();
zip.write_all(
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>
</Types>",
)
.unwrap();
zip.start_file("_rels/.rels", options).unwrap();
let rels = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">
<Relationship Target=\"/2D/testÆfile.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>
</Relationships>";
zip.write_all(rels.as_bytes()).unwrap();
zip.start_file("2D/testÆfile.model", options).unwrap();
zip.write_all(
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<model unit=\"millimeter\" xml:lang=\"en-US\" xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\">
<resources>
<object id=\"1\" type=\"model\">
<mesh>
<vertices>
<vertex x=\"0\" y=\"0\" z=\"0\"/>
<vertex x=\"100\" y=\"0\" z=\"0\"/>
<vertex x=\"0\" y=\"100\" z=\"0\"/>
</vertices>
<triangles>
<triangle v1=\"0\" v2=\"1\" v3=\"2\"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid=\"1\"/>
</build>
</model>",
)
.unwrap();
let cursor = zip.finish().unwrap();
let result = Package::open(cursor);
assert!(
result.is_ok(),
"Package with UTF-8 characters in XML should be accepted for compatibility"
);
}
#[test]
fn test_lenient_accepts_nonstandard_thumbnail_rel_type() {
let content_types = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Default Extension=\"png\" ContentType=\"image/png\"/>\
</Types>";
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/Metadata/thumbnail.png\" Id=\"rel1\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-middle\"/>\
</Relationships>";
let png: &[u8] = &[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00,
0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08,
0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xE2, 0x21, 0xBC,
0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
];
let cursor = make_zip(&[
("[Content_Types].xml", content_types),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
("Metadata/thumbnail.png", png),
]);
let strict_result = Package::open(cursor.clone());
assert!(
strict_result.is_err(),
"Strict mode should reject non-standard thumbnail relationship type"
);
let lenient_result = Package::open_lenient(cursor, true);
assert!(
lenient_result.is_ok(),
"Lenient mode should accept non-standard thumbnail relationship type"
);
}
#[test]
fn test_lenient_accepts_duplicate_relationship_ids() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/Metadata/something.xml\" Id=\"rel0\" Type=\"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
("Metadata/something.xml", b"<root/>"),
]);
let strict_result = Package::open(cursor.clone());
assert!(
strict_result.is_err(),
"Strict mode should reject duplicate relationship IDs"
);
let lenient_result = Package::open_lenient(cursor, true);
assert!(
lenient_result.is_ok(),
"Lenient mode should accept duplicate relationship IDs"
);
}
#[test]
fn test_lenient_accepts_id_starting_with_digit() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"0rel\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let strict_result = Package::open(cursor.clone());
assert!(
strict_result.is_err(),
"Strict mode should reject ID starting with digit"
);
let lenient_result = Package::open_lenient(cursor, true);
assert!(
lenient_result.is_ok(),
"Lenient mode should accept ID starting with digit"
);
}
#[test]
fn test_lenient_accepts_duplicate_content_type_defaults() {
let content_types = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
</Types>";
let cursor = make_zip(&[
("[Content_Types].xml", content_types),
("_rels/.rels", MINIMAL_RELS),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let strict_result = Package::open(cursor.clone());
assert!(
strict_result.is_err(),
"Strict mode should reject duplicate content type defaults"
);
let lenient_result = Package::open_lenient(cursor, true);
assert!(
lenient_result.is_ok(),
"Lenient mode should accept duplicate content type defaults"
);
}
#[test]
fn test_lenient_accepts_nonexistent_relationship_target() {
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/Metadata/missing.xml\" Id=\"rel1\" Type=\"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties\"/>\
</Relationships>";
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", rels),
("3D/3dmodel.model", MINIMAL_MODEL),
]);
let strict_result = Package::open(cursor.clone());
assert!(
strict_result.is_err(),
"Strict mode should reject relationship pointing to non-existent file"
);
let lenient_result = Package::open_lenient(cursor, true);
assert!(
lenient_result.is_ok(),
"Lenient mode should accept relationship pointing to non-existent file"
);
}
#[test]
fn test_lenient_still_rejects_missing_model() {
let cursor = make_zip(&[
("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
("_rels/.rels", MINIMAL_RELS),
]);
let lenient_result = Package::open_lenient(cursor, true);
assert!(
lenient_result.is_err(),
"Lenient mode should still reject packages missing the model file"
);
}
#[test]
fn test_lenient_parserconfig_integration() {
use crate::model::{ParserConfig, SpecConformance};
use crate::parser::parse_3mf_with_config;
let rels = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\
<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\
<Relationship Target=\"/Metadata/thumbnail.png\" Id=\"rel1\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-middle\"/>\
</Relationships>";
let content_types = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\
<Default Extension=\"png\" ContentType=\"image/png\"/>\
</Types>";
let model = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<model unit=\"millimeter\" xml:lang=\"en-US\" \
xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\">\
<resources>\
<object id=\"1\" type=\"model\">\
<mesh>\
<vertices>\
<vertex x=\"0\" y=\"0\" z=\"0\"/>\
<vertex x=\"1\" y=\"0\" z=\"0\"/>\
<vertex x=\"0\" y=\"1\" z=\"0\"/>\
<vertex x=\"0\" y=\"0\" z=\"1\"/>\
</vertices>\
<triangles>\
<triangle v1=\"0\" v2=\"1\" v3=\"2\"/>\
<triangle v1=\"0\" v2=\"1\" v3=\"3\"/>\
<triangle v1=\"0\" v2=\"2\" v3=\"3\"/>\
<triangle v1=\"1\" v2=\"2\" v3=\"3\"/>\
</triangles>\
</mesh>\
</object>\
</resources>\
<build><item objectid=\"1\"/></build>\
</model>";
let png: &[u8] = &[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00,
0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08,
0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xE2, 0x21, 0xBC,
0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
];
let cursor = make_zip(&[
("[Content_Types].xml", content_types),
("_rels/.rels", rels),
("3D/3dmodel.model", model),
("Metadata/thumbnail.png", png),
]);
let config = ParserConfig::with_all_extensions();
assert!(config.spec_conformance() == SpecConformance::Strict);
let strict_result = parse_3mf_with_config(cursor.clone(), config);
assert!(strict_result.is_err());
let config =
ParserConfig::with_all_extensions().with_spec_conformance(SpecConformance::Lenient);
assert!(config.is_lenient());
let lenient_result = parse_3mf_with_config(cursor, config);
assert!(
lenient_result.is_ok(),
"parse_3mf_with_config with Lenient should accept non-standard thumbnail: {:?}",
lenient_result.err()
);
}
}