Skip to content

Contributing to OxiDex ​

Thank you for your interest in contributing to OxiDex! This guide will help you get started with development, testing, and submitting contributions.

Getting Started ​

Prerequisites ​

  • Rust: 1.75+ (install via rustup)
  • Git: Version control
  • C Compiler: GCC or Clang (for building dependencies)
  • Optional: ExifTool Perl (for comparison tests)

Development Setup ​

  1. Clone the repository:
bash
git clone https://github.com/swack-tools/oxidex.git
cd oxidex
  1. Build the project:
bash
cargo build --release

Note: Always use --release flag due to tag database memory requirements. Debug builds will OOM (>32GB RAM).

  1. Run tests:
bash
cargo test --release
  1. Install development tools:
bash
# Formatter
rustup component add rustfmt

# Linter
rustup component add clippy

# Benchmark runner
cargo install criterion

# Code coverage
cargo install cargo-tarpaulin

Project Structure ​

oxidex/
├── src/
│   ├── bin/              # CLI binary
│   ├── core/             # Core metadata types
│   ├── parsers/          # Format-specific parsers
│   │   ├── jpeg/
│   │   ├── tiff/
│   │   ├── png/
│   │   └── ...
│   ├── ffi/              # C FFI bindings
│   └── lib.rs            # Library entry point
├── exiftool-tags/        # Tag database (separate crate)
├── tests/                # Integration tests
├── benches/              # Benchmarks
├── docs/                 # Documentation
└── examples/             # Example code

Development Workflow ​

1. Create a Branch ​

bash
git checkout -b feature/your-feature-name
# or
git checkout -b fix/bug-description

Branch naming:

  • feature/ - New features
  • fix/ - Bug fixes
  • perf/ - Performance improvements
  • docs/ - Documentation updates
  • refactor/ - Code refactoring

2. Make Changes ​

Follow the Coding Standards below.

3. Test Your Changes ​

bash
# Run all tests
cargo test --release

# Run specific test
cargo test --release test_name

# Run with output
cargo test --release -- --nocapture

4. Format and Lint ​

bash
# Format code
cargo fmt

# Check formatting
cargo fmt -- --check

# Run linter
cargo clippy --release -- -D warnings

5. Commit Your Changes ​

Use conventional commit messages:

bash
git commit -m "feat: add HEIC format support"
git commit -m "fix: resolve JPEG thumbnail corruption"
git commit -m "perf: optimize TIFF IFD parsing"
git commit -m "docs: update API reference"

Commit message format:

<type>: <description>

[optional body]

[optional footer]

Types:

  • feat - New feature
  • fix - Bug fix
  • perf - Performance improvement
  • docs - Documentation changes
  • test - Test additions/changes
  • refactor - Code refactoring
  • chore - Maintenance tasks

6. Push and Create Pull Request ​

bash
git push origin feature/your-feature-name

Then create a pull request on GitHub.

Coding Standards ​

Rust Style Guide ​

Follow the Rust API Guidelines:

Naming Conventions:

rust
// Types: PascalCase
struct MetadataMap { }
enum TagValue { }

// Functions: snake_case
fn parse_jpeg(data: &[u8]) -> Result<Metadata> { }

// Constants: SCREAMING_SNAKE_CASE
const MAX_TAG_SIZE: usize = 65536;

// Modules: snake_case
mod jpeg_parser;

Error Handling:

rust
// Use Result for fallible operations
fn read_file(path: &Path) -> Result<Metadata, ExifToolError> {
    let file = File::open(path)?;
    parse_metadata(file)
}

// Provide context in errors
Err(ExifToolError::ParseError {
    message: format!("Invalid JPEG marker: {:02X}", marker),
    offset: Some(offset),
})

Documentation:

rust
/// Parses JPEG metadata from raw bytes.
///
/// # Arguments
///
/// * `data` - Raw JPEG file data
///
/// # Returns
///
/// * `Ok(Metadata)` - Parsed metadata
/// * `Err(ExifToolError)` - Parse error with context
///
/// # Examples
///
/// ```
/// use oxidex::parsers::jpeg::parse_jpeg;
///
/// let data = std::fs::read("photo.jpg")?;
/// let metadata = parse_jpeg(&data)?;
/// ```
pub fn parse_jpeg(data: &[u8]) -> Result<Metadata, ExifToolError> {
    // Implementation
}

Code Comments ​

When to comment:

  • Complex algorithms
  • Non-obvious optimizations
  • Workarounds for external limitations
  • Magic numbers (explain what they represent)

When NOT to comment:

  • Self-explanatory code
  • Obvious operations
  • Every line (let code speak for itself)

Good comments:

rust
// JPEG markers are big-endian 16-bit values starting with 0xFF
let marker = u16::from_be_bytes([data[0], data[1]]);

// Skip thumbnail data (already extracted in previous pass)
if tag_id == 0x0201 { continue; }

// Per EXIF spec, GPS tags use different byte order than main IFD
let gps_byte_order = detect_gps_byte_order(data);

Testing Standards ​

Unit Tests:

rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_jpeg_marker() {
        let data = [0xFF, 0xD8];  // JPEG SOI marker
        let marker = parse_marker(&data).unwrap();
        assert_eq!(marker, JpegMarker::SOI);
    }

    #[test]
    fn test_parse_invalid_marker() {
        let data = [0x00, 0x00];  // Invalid
        assert!(parse_marker(&data).is_err());
    }
}

Integration Tests:

rust
// tests/integration_tests.rs
use oxidex::Metadata;

#[test]
fn test_read_real_jpeg() {
    let metadata = Metadata::from_path("tests/fixtures/sample.jpg")
        .expect("Failed to read test file");

    assert_eq!(metadata.get_string("EXIF:Make").unwrap(), "Canon");
    assert_eq!(metadata.get_integer("EXIF:ISO").unwrap(), 400);
}

Benchmarking ​

Add benchmarks for performance-critical code:

rust
// benches/parse_benchmarks.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use oxidex::parsers::jpeg::parse_jpeg;

fn benchmark_jpeg_parsing(c: &mut Criterion) {
    let data = std::fs::read("tests/fixtures/large.jpg").unwrap();

    c.bench_function("parse_jpeg", |b| {
        b.iter(|| parse_jpeg(black_box(&data)))
    });
}

criterion_group!(benches, benchmark_jpeg_parsing);
criterion_main!(benches);

Run benchmarks:

bash
cargo bench

Adding New Features ​

Adding a New File Format ​

  1. Create parser module:
bash
mkdir -p src/parsers/your_format
touch src/parsers/your_format/mod.rs
  1. Implement format detection:
rust
// src/formats/mod.rs
pub fn detect_format(data: &[u8]) -> Option<FileFormat> {
    match &data[0..4] {
        // Your format magic number
        [0x89, b'Y', b'O', b'U'] => Some(FileFormat::YourFormat),
        // ... other formats
        _ => None,
    }
}
  1. Implement parser:
rust
// src/parsers/your_format/mod.rs
use crate::core::{Metadata, TagValue};
use crate::error::{Result, ExifToolError};

pub fn parse_your_format(data: &[u8]) -> Result<Metadata> {
    let mut metadata = Metadata::new();

    // Parse format-specific structures
    // Extract tags
    // Populate metadata

    Ok(metadata)
}
  1. Add tests:
rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_your_format() {
        let data = include_bytes!("../../../tests/fixtures/sample.your");
        let metadata = parse_your_format(data).unwrap();
        assert!(metadata.len() > 0);
    }
}
  1. Add integration test:

Create test file in tests/fixtures/ and add integration test.

  1. Update documentation:

Add format to docs/formats.md with:

  • File extensions
  • Supported metadata types
  • Tag count
  • Common use cases

Adding New Tags ​

  1. Tags are auto-generated from ExifTool source

The tag database is automatically synchronized. To add custom tags:

rust
// For custom tags not in ExifTool
metadata.insert("Custom:YourTag", TagValue::new_string("value"));
  1. To update from latest ExifTool:
bash
rm exiftool-tags/src/tag_db/generated_tags.rs
cargo build --release

Testing ​

Running Tests ​

bash
# All tests (always use --release)
cargo test --release

# Specific module
cargo test --release parsers::jpeg

# With output
cargo test --release -- --nocapture

# Single test
cargo test --release test_parse_jpeg_marker

Test Data ​

Place test files in tests/fixtures/:

tests/fixtures/
├── jpeg/
│   ├── sample_with_exif.jpg
│   ├── sample_no_metadata.jpg
│   └── corrupted.jpg
├── tiff/
├── png/
└── ...

Code Coverage ​

bash
cargo tarpaulin --release --out Html
open tarpaulin-report.html

Pull Request Guidelines ​

Before Submitting ​

  • [ ] Code compiles without warnings: cargo build --release
  • [ ] All tests pass: cargo test --release
  • [ ] Code is formatted: cargo fmt -- --check
  • [ ] Linter passes: cargo clippy --release -- -D warnings
  • [ ] Documentation updated if needed
  • [ ] Benchmarks added for performance-critical changes
  • [ ] CHANGELOG.md updated (for notable changes)

PR Description Template ​

markdown
## Description

Brief description of changes.

## Type of Change

- [ ] Bug fix
- [ ] New feature
- [ ] Performance improvement
- [ ] Documentation update
- [ ] Refactoring

## Testing

Describe testing done:
- Unit tests added/modified
- Integration tests added
- Manual testing performed

## Checklist

- [ ] Code follows project style guidelines
- [ ] Self-review completed
- [ ] Comments added for complex code
- [ ] Documentation updated
- [ ] Tests pass locally
- [ ] No new warnings introduced

Review Process ​

  1. Automated Checks: CI runs tests, linting, benchmarks
  2. Code Review: Maintainer reviews code quality, design
  3. Feedback: Address review comments
  4. Approval: Maintainer approves and merges

Performance Guidelines ​

Optimization Principles ​

  1. Measure first: Profile before optimizing
  2. Focus on hot paths: Optimize frequently-called code
  3. Avoid premature optimization: Clarity > premature speed
  4. Document trade-offs: Explain complex optimizations

Using Profiling ​

bash
# Install samply
cargo install samply

# Profile benchmark
samply record target/release/oxidex photo.jpg

# Opens Firefox Profiler with results

See Profiling Guide for details.

Benchmarking Changes ​

bash
# Baseline
cargo bench
cp -r target/criterion target/criterion-baseline

# Make changes
# ...

# Re-benchmark
cargo bench

# Compare results in target/criterion/report/index.html

Documentation ​

Building the Documentation Site ​

The site is VitePress and lives in docs/. A fresh clone builds with no extra setup:

bash
cd docs
npm ci
npm run docs:build   # or: npm run docs:dev

About docs/reference/comparison/. The per-format ExifTool comparison report is a generated artifact, not a checked-in one — the directory is listed in .gitignore and the deploy workflow regenerates it on every deploy. So on a clean checkout it is empty.

You do not need to generate it to build or preview the docs. npm run docs:build runs scripts/ensure-comparison-stub.mjs first, which drops a clearly-labelled placeholder page at /reference/comparison/ so every link resolves and the build succeeds. If you want the real tables locally:

bash
just compare-exiftool-full-update

That downloads a sample corpus, builds the tag-comparison binary in release mode and runs it against a real ExifTool install, so it needs a Rust toolchain plus Perl and takes a while. Once it has run, the stub script sees the generated index.md and leaves it alone.

The Docs Build CI workflow builds the site from a clean checkout on every PR that touches docs/, deliberately without running the generator — that is what keeps the cold-clone build honest.

Two markdown patterns that silently break the build ​

VitePress compiles markdown through Vue, so two things that look like prose are treated as code and fail the build. Both have broken this site before:

  • A line that starts with a bare angle-bracket tag is parsed as a Vue component.
  • Unfenced double curly braces are parsed as a Vue interpolation.

Keep either inside a fenced code block or inline backticks and they are safe.

Inline Documentation ​

Use rustdoc for all public APIs:

rust
/// Parses JPEG metadata.
///
/// # Arguments
///
/// * `data` - Raw file bytes
///
/// # Errors
///
/// Returns error if:
/// - File is not valid JPEG
/// - Metadata is corrupted
pub fn parse_jpeg(data: &[u8]) -> Result<Metadata> {
    // ...
}

User Documentation ​

Update relevant documentation in docs/:

  • docs/formats.md - Format support
  • docs/api.md - API changes
  • docs/cli.md - CLI changes

Getting Help ​

  • GitHub Discussions: Ask questions
  • GitHub Issues: Report bugs, request features
  • Discord: Join community chat (link in README)

Code of Conduct ​

Be respectful, inclusive, and collaborative. See CODE_OF_CONDUCT.md for full guidelines.

License ​

By contributing, you agree that your contributions will be licensed under the GNU General Public License v3.0 (GPL-3.0).

Additional Resources ​

Released under the GPL-3.0 License.