Contributing
Thank you for your interest in contributing to lib3mf-rs! This guide covers development setup, testing strategies, code conventions, and how to add new features.
Development Setup
Prerequisites
- Rust toolchain (stable, v1.70 or later)
- Git
Clone and Build
git clone https://github.com/sscargal/lib3mf-rs.git
cd lib3mf-rs
# Debug build (fast compilation)
cargo build
# Run tests
cargo test
# Run linter
cargo clippy -- -D warnings
# Format code
cargo fmt
Project Structure
lib3mf-rs/
├── crates/
│ ├── lib3mf-core/ # Main library
│ ├── lib3mf-cli/ # CLI tool
│ ├── lib3mf-converters/ # Format converters
│ ├── lib3mf-async/ # Async I/O
│ └── lib3mf-wasm/ # WebAssembly bindings
├── fuzz/ # Fuzzing targets
├── book/ # This documentation
├── scripts/ # QA and utility scripts
└── .github/workflows/ # CI/CD pipelines
Build Commands
Standard Builds
# Debug build (fast compilation, slower execution)
cargo build
# Release build (optimized for performance)
cargo build --release
# Build specific crate
cargo build -p lib3mf-core
cargo build -p lib3mf-cli
Feature-Specific Builds
# Minimal build (no optional dependencies)
cargo build -p lib3mf-core --no-default-features
# With crypto support
cargo build -p lib3mf-core --features crypto
# With parallel processing
cargo build -p lib3mf-core --features parallel
# All features
cargo build -p lib3mf-core --all-features
Check Dependency Count
# Verify minimal build dependency count
cargo tree -p lib3mf-core --no-default-features | wc -l
# Expected: ~154 crates
# Full-featured build
cargo tree -p lib3mf-core --all-features | wc -l
# Expected: ~300 crates
Testing
Running Tests
# Run all tests
cargo test
# Run tests for specific crate
cargo test -p lib3mf-core
# Run tests with specific features
cargo test -p lib3mf-core --no-default-features
cargo test -p lib3mf-core --features crypto
cargo test -p lib3mf-core --all-features
# Run property-based tests
cargo test -p lib3mf-core --test proptests
Test Organization
Unit tests — Located in same file as code, in #[cfg(test)] mod tests blocks:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_vertex() {
// Test implementation
}
}
Integration tests — Located in tests/ directories:
crates/lib3mf-core/tests/
├── parse_tests.rs # Full file parsing
├── roundtrip_tests.rs # Parse → Write → Parse
├── validation_tests.rs # Validation system
└── extension_tests.rs # Extension-specific tests
Property-based tests — Using proptest for robustness:
use proptest::prelude::*;
proptest! {
#[test]
fn test_vertex_roundtrip(x in -1000.0f32..1000.0, y in -1000.0..1000.0, z in -1000.0..1000.0) {
let vertex = Vertex::new(x, y, z);
let xml = write_vertex(&vertex);
let parsed = parse_vertex(&xml)?;
assert_eq!(vertex, parsed);
}
}
Fuzzing
Fuzzing tests for security and robustness using cargo-fuzz:
Setup:
# Install nightly toolchain (required for fuzzing)
rustup toolchain install nightly
# Install cargo-fuzz
cargo +nightly install cargo-fuzz
Running fuzz targets:
# List available targets
cargo +nightly fuzz list
# Run a specific target (indefinitely until Ctrl+C)
cargo +nightly fuzz run parse_model
# Run with time limit (60 second smoke test)
cargo +nightly fuzz run parse_model -- -max_total_time=60
# Run with dictionary for better coverage
cargo +nightly fuzz run parse_xml -- -dict=fuzz/dictionaries/3mf.dict
Available fuzz targets:
parse_model— Full 3MF file parsing (ZIP + XML + invariants)parse_xml— Direct XML model parsing (bypasses ZIP)parse_materials— Material/texture parsing isolationparse_crypto— Signature/encryption parsingparse_extensions— Extension-specific parsersparse_opc— OPC relationship parsingwriter_roundtrip— Fuzz writer by round-tripping models
Crash handling:
If fuzzing finds a crash:
# Minimize crash file (reduce to smallest reproducer)
cargo +nightly fuzz tmin parse_model fuzz/artifacts/parse_model/crash-abc123
# Add to regression tests
cp fuzz/artifacts/parse_model/crash-abc123 crates/lib3mf-core/tests/fuzz_regression/parse_model_001.bin
# Create test case
#[test]
fn test_fuzz_regression_001() {
let data = include_bytes!("fuzz_regression/parse_model_001.bin");
// Verify it doesn't crash or produces expected error
let result = parse_model(Cursor::new(data));
assert!(result.is_err()); // Should error gracefully, not panic
}
QA Test Suite
Run comprehensive validation before submitting PRs:
./scripts/qa_test_suite.sh
This script runs:
- Format check (
cargo fmt --check) - Linter (
cargo clippy) - All tests with all feature combinations
- Benchmarks
- Example runs
- CLI validation
Expected runtime: ~5 minutes
Code Quality Standards
Formatting
Use rustfmt with default settings:
cargo fmt
Before committing:
cargo fmt --check
Linting
Use clippy with warnings as errors:
cargo clippy -- -D warnings
Common clippy warnings to avoid:
- Unnecessary clones
- Inefficient string operations
- Missing error propagation
- Unsafe code without justification
Documentation
Public items must have doc comments:
/// Parses a 3MF model from an XML reader.
///
/// # Arguments
///
/// * `reader` - The XML data source
///
/// # Returns
///
/// A `Model` instance on success, or `Lib3mfError` on parse failure.
///
/// # Examples
///
/// ```
/// use lib3mf_core::parser::parse_model;
/// use std::io::Cursor;
///
/// let xml = r#"<model>...</model>"#;
/// let model = parse_model(Cursor::new(xml))?;
/// # Ok::<(), lib3mf_core::error::Lib3mfError>(())
/// ```
pub fn parse_model<R: Read>(reader: R) -> Result<Model> {
// Implementation
}
Module-level documentation:
//! Mesh repair utilities for fixing geometry issues.
//!
//! This module provides the `MeshRepair` trait and implementations
//! for common repair operations like vertex stitching and orientation
//! harmonization.
Code Conventions
Immutable Design
Prefer immutable data structures:
// Good: Return new instance
pub fn stitch_vertices(&self, epsilon: f32) -> Result<Mesh> {
let mut new_mesh = self.clone();
// Perform stitching on new_mesh
Ok(new_mesh)
}
// Avoid: Mutate in place (unless explicitly required)
pub fn stitch_vertices_mut(&mut self, epsilon: f32) {
// Avoid this pattern
}
Rationale: Thread safety, predictable behavior, easier testing.
Error Handling
Use Result<T> for fallible operations:
use crate::error::{Lib3mfError, Result};
pub fn parse_vertex(xml: &str) -> Result<Vertex> {
let x = xml.parse::<f32>()
.map_err(|e| Lib3mfError::ParseError(format!("Invalid X coordinate: {}", e)))?;
// ...
Ok(Vertex { x, y, z })
}
Never panic in library code:
// Bad
pub fn get_object(&self, id: ResourceId) -> Object {
self.objects.get(&id).unwrap() // DON'T DO THIS
}
// Good
pub fn get_object(&self, id: ResourceId) -> Option<&Object> {
self.objects.get(&id)
}
// Or
pub fn get_object(&self, id: ResourceId) -> Result<&Object> {
self.objects.get(&id)
.ok_or_else(|| Lib3mfError::ResourceNotFound(id))
}
Trait Abstractions
Use traits for extensibility:
pub trait ArchiveReader {
fn read_entry(&mut self, path: &str) -> Result<Vec<u8>>;
fn entry_names(&mut self) -> Result<Vec<String>>;
}
impl ArchiveReader for ZipArchiver {
// Implementation
}
Resource Management
Use newtype pattern for type-safe IDs:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ResourceId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TextureId(pub u32);
// Prevents mixing different ID types
Adding New Features
Adding a New Extension
Extensions require integration across multiple layers:
- Define data structures (
model/module):
// In model/geometry.rs or new file
pub struct MyExtensionData {
pub property_a: String,
pub property_b: f32,
}
// Add to Geometry enum if geometry-related
pub enum Geometry {
Mesh(Mesh),
BeamLattice(BeamLattice),
MyExtension(MyExtensionData), // Add here
}
- Create parser (
parser/my_extension_parser.rs):
use quick_xml::Reader;
use crate::model::MyExtensionData;
pub fn parse_my_extension<R: BufRead>(reader: &mut Reader<R>) -> Result<MyExtensionData> {
// XML parsing logic
}
- Integrate into main parser (
parser/model_parser.rs):
match event {
// Existing cases...
Start(ref e) if e.local_name().as_ref() == b"myextension" => {
let ext_data = parse_my_extension(reader)?;
// Add to model
}
}
- Add writer (
writer/my_extension_writer.rs):
pub fn write_my_extension<W: Write>(writer: &mut Writer<W>, data: &MyExtensionData) -> Result<()> {
// XML writing logic
}
- Add validation (
validation/validator.rs):
fn validate_my_extension(&self, data: &MyExtensionData, report: &mut ValidationReport) {
if data.property_b < 0.0 {
report.add_error(5100, "Property B must be non-negative");
}
}
- Write tests (
tests/my_extension_tests.rs):
#[test]
fn test_my_extension_roundtrip() {
let xml = r#"<myextension property_a="test" property_b="1.5" />"#;
let parsed = parse_my_extension(xml)?;
assert_eq!(parsed.property_a, "test");
assert_eq!(parsed.property_b, 1.5);
}
- Add example (
examples/my_extension_demo.rs):
//! Demonstrates using MyExtension feature
use lib3mf_core::Model;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let model = Model::from_file("example.3mf")?;
// Show extension usage
Ok(())
}
- Update documentation:
- Add to
book/src/extensions.md - Update rustdoc comments
- Update README.md feature list
- Add to
Adding a New Resource Type
Similar process but focuses on ResourceCollection:
- Define struct in
model/resources.rs - Add to
ResourceCollectionwithadd_*/get_*methods - Implement parser and writer
- Add validation rules
- Write tests and examples
Feature Flag Testing
Test all feature combinations in CI:
# Test minimal build
cargo test -p lib3mf-core --no-default-features
# Test crypto only
cargo test -p lib3mf-core --no-default-features --features crypto
# Test parallel only
cargo test -p lib3mf-core --no-default-features --features parallel
# Test all features
cargo test -p lib3mf-core --all-features
CI matrix (.github/workflows/ci.yml):
strategy:
matrix:
features:
- "--no-default-features"
- "--features crypto"
- "--features parallel"
- "--all-features"
Pull Request Process
- Fork and clone the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Make changes following code conventions
- Write tests (unit + integration)
- Run QA suite (
./scripts/qa_test_suite.sh) - Commit with clear messages:
feat: add MyExtension support - Parse myextension elements - Add MyExtensionData to Geometry enum - Write tests and example - Update documentation - Push and create PR
- Address review feedback
Finding Work
Check GitHub Issues for:
- Good First Issue — Beginner-friendly tasks
- Help Wanted — Community contributions welcome
- Bug — Reported bugs needing fixes
- Enhancement — Feature requests
Or propose your own improvements!
Questions?
- GitHub Issues — For bugs and feature requests
- GitHub Discussions — For questions and ideas
- Email — steve@scargall.com
License
By contributing, you agree that your contributions will be licensed under the MIT License.