# Contributing to Synta
Thank you for your interest in contributing to Synta. This document describes
everything you need to know: how to set up a development environment, the
contribution workflow, what we expect from licensing and sign-off, how to run
the same checks that CI runs, and how to write good commit messages.
## Table of Contents
---
## Licensing and Sign-off
### Dual licence
Synta is dual-licensed under **MIT OR Apache-2.0**. By submitting a
contribution you agree that your work will be made available under those same
terms and that you have the right to do so.
When adding new source files, include a SPDX header at the top:
```rust
// SPDX-License-Identifier: MIT OR Apache-2.0
```
### Developer Certificate of Origin
Synta uses the [Developer Certificate of Origin (DCO)](https://developercertificate.org/)
instead of a Contributor Licence Agreement (CLA). Every commit must carry a
`Signed-off-by` trailer certifying that you wrote the code or have the right
to submit it under the project licence.
Add the sign-off with the `-s` flag when committing:
```bash
git commit -s
```
This appends a line like the following to your commit message:
```
Signed-off-by: Jane Smith <jane@example.com>
```
Commits without a `Signed-off-by` line will not be accepted. If you are
contributing on behalf of an employer, ensure your employer permits open-source
contributions before submitting.
### AI-assisted contributions
AI coding tools (such as GitHub Copilot, Claude, or similar) may be used to
assist with writing code, tests, and documentation. The following rules apply:
- **You are responsible for everything you submit.** Review, understand, and
test any AI-generated content before including it in a PR. Submitting
output you cannot explain or vouch for is not acceptable.
- **The DCO sign-off still applies.** Signing off with `git commit -s`
certifies that you have the right to submit the contribution and that you
understand its content — regardless of how it was produced.
- **All CI checks must pass.** AI-generated code is subject to the same
quality bar as hand-written code: `cargo fmt`, `clippy -D warnings`, tests,
and documentation sample validation.
- **Disclose AI assistance in the PR description** when a significant portion
of the contribution was generated or substantially rewritten by an AI tool.
A brief note is sufficient; the goal is transparency, not restriction.
---
## Contribution Workflow
The canonical repository is at
[codeberg.org/abbra/synta](https://codeberg.org/abbra/synta).
Pull requests should target the `main` branch.
### 1. Fork and clone
Fork the repository on Codeberg, then clone your fork:
```bash
git clone ssh://git@codeberg.org/<your-username>/synta.git
cd synta
git remote add upstream https://codeberg.org/abbra/synta.git
```
### 2. Create a branch
Always work on a dedicated branch, not directly on `main`:
```bash
git fetch upstream
git checkout -b my-feature upstream/main
```
Choose a short, descriptive branch name (`fix-ber-length-decoding`,
`add-generalizedtime-serde`, `krb5-pa-data-types`, etc.).
### 3. Make your changes
Implement your change and add tests (see [Writing Tests](#writing-tests)).
Keep each commit focused on a single concern; avoid bundling unrelated fixes
in the same PR.
### 4. Run local CI
Run the full CI suite before pushing to catch problems early:
```bash
./contrib/ci/local-ci.sh all
```
See [Running CI Locally](#running-ci-locally) for details on individual jobs
and flags.
### 5. Commit
Use `git commit -s` to sign off each commit. Follow the
[commit message conventions](#commit-message-conventions) described below.
### 6. Open a pull request
Push your branch and open a pull request against `abbra/synta:main`:
```bash
git push origin my-feature
```
Then open a PR on Codeberg. In the PR description:
- Summarise what the change does and why.
- Reference any related issues with `Fixes #NNN` or `See #NNN`.
- Mention if the change is a breaking API change.
The CI pipeline runs automatically on every PR. All jobs must pass before
a PR can be merged.
### 7. Review and merge
A maintainer will review your PR. If changes are requested:
- Push additional commits; do not force-push (it makes review history harder
to follow).
- Mark review comments as resolved after addressing them.
Once the PR is approved and CI is green it will be merged.
---
## Commit Message Conventions
Synta uses
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) with a
scope in parentheses:
```
<type>(<scope>): <short description>
<optional body — wrap at 72 characters>
Fixes: https://codeberg.org/abbra/synta/issues/<NNN>
Signed-off-by: Jane Smith <jane@example.com>
```
`Fixes:` is optional; include it when the commit resolves a Codeberg issue.
Use the full URL so the reference is unambiguous in any context. Multiple
`Fixes:` lines are allowed when a commit addresses several issues.
`Signed-off-by:` is required on every commit (see
[Developer Certificate of Origin](#developer-certificate-of-origin)).
Add it with `git commit -s`.
**Commit message template**
The repository ships a commit message template at `.gitmessage` that
pre-fills the format, lists all valid types and scopes, and keeps the
`Fixes:` and `Signed-off-by:` trailers visible as a reminder. Activate it
once after cloning:
```bash
git config commit.template .gitmessage
```
After that, `git commit` (without `-m`) opens your editor with the template
pre-loaded. Use `git commit -s` to have the `Signed-off-by:` trailer
appended automatically.
**Types:**
| `feat` | New feature or capability |
| `fix` | Bug fix |
| `perf` | Performance improvement |
| `refactor` | Restructuring without behaviour change |
| `test` | Adding or fixing tests |
| `docs` | Documentation only |
| `ci` | CI configuration |
| `build` | Build system / dependency changes |
| `chore` | Maintenance tasks that don't fit elsewhere |
**Scope** is the crate or subsystem affected: `synta`, `synta-ffi`,
`synta-python`, `synta-derive`, `synta-codegen`, `synta-certificate`,
`synta-krb5`, `synta-bench`, `contrib/ci`, `docs`, etc.
**Examples:**
```
feat(synta): add lazy Sequence iterator for zero-copy traversal
fix(synta-ffi): prevent double-free in synta_decoder_free on NULL input
Fixes: https://codeberg.org/abbra/synta/issues/42
Signed-off-by: Jane Smith <jane@example.com>
```
```
perf(synta): replace eager Vec allocation in OctetString decode
docs(synta-python): document Certificate.subject_public_key_info field
ci(github): add toc job to GitHub Actions workflow
```
Keep the subject line under 72 characters and written in the imperative mood
("add", "fix", "remove" — not "added", "fixes", "removing").
---
## Code Style
### Rust
**Formatting** is enforced by `rustfmt` with the project defaults (no
`rustfmt.toml`). Run before committing:
```bash
cargo fmt --all
```
CI fails if any file would be reformatted.
**Linting** is enforced by Clippy with all warnings treated as errors:
```bash
cargo clippy --workspace -- -D warnings
```
Fix all Clippy diagnostics before opening a PR. If a lint is a false
positive, suppress it with `#[allow(...)]` on the smallest possible scope and
add a comment explaining why.
**API documentation** is enforced with `RUSTDOCFLAGS=-D warnings`:
```bash
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
```
Public items must have doc comments. Code examples in doc comments are
compiled and type-checked by the `doc-rust` CI job.
**Style guidelines:**
- Prefer zero-copy types (`&[u8]`, `&str`, `RawDer<'_>`) over owned
equivalents where the API permits it.
- Avoid unnecessary heap allocations on hot paths; the benchmark suite will
catch regressions.
- Use `#[derive(Debug, Clone, PartialEq)]` on public types where appropriate.
- Prefer `impl Trait` in argument position over generic bounds for readability
when there is only one trait bound.
- Error types should implement `std::error::Error` and be non-exhaustive
(`#[non_exhaustive]`) when public.
### Python
Python files in `contrib/` and `python/` are linted with
[Ruff](https://docs.astral.sh/ruff/):
```bash
ruff check contrib/
ruff format --check contrib/
```
Fix issues before committing. CI runs Ruff via `uv` when available.
### Markdown
Every Markdown file must have an up-to-date doctoc-compatible table of
contents block (see [Markdown docs](#markdown-docs)). After editing a
document, regenerate its TOC:
```bash
./contrib/ci/local-ci.sh --update toc
```
---
## Development Environment
### Required tools
| **Bash 4+** | Required by `contrib/ci/local-ci.sh` and all other shell scripts. macOS users may need to install a newer bash via Homebrew (`brew install bash`), as macOS ships with bash 3.2. Each script checks the version on startup and exits with an error if bash 3 or earlier is detected. |
| Rust stable toolchain | Install via [rustup](https://rustup.rs/). Must include the `rustfmt` and `clippy` components (`rustup component add rustfmt clippy`). |
| C compiler and `make` | `gcc` or `clang`, plus GNU `make`. Required by the `c-test` job. |
| Python 3.8+ | Required by the Python bindings (`synta-python`) and by `contrib/` tooling. |
### Optional tools
| `uv` | Recommended for managing Python environments. Install from [astral.sh/uv](https://astral.sh/uv/). The local CI script falls back to `python3 -m venv` + `pip` when `uv` is absent. |
| `cargo-c` | Required for `cargo cbuild` in the `build` job, which produces the host-triple–qualified library consumed by `synta-ffi/examples/c/`. Install with `cargo install cargo-c`. Skipped with a warning when absent; the `c-test` job is unaffected. |
| `cbindgen` | Required to regenerate `include/synta.h` from Rust code. Invoked automatically by `synta-ffi/build.rs` during `cargo build`. Install with `cargo install cbindgen`. Build succeeds with a warning when absent, but the header remains stale. |
| `valgrind` | Required only when running `./contrib/ci/local-ci.sh --valgrind`. Memory-checks both C and Rust test binaries. |
| `actionlint` | Validates GitHub Actions workflow files. Falls back to `yamllint` when absent. |
### System libraries
The C-based jobs require the following native libraries. On Debian/Ubuntu:
```bash
apt-get install build-essential libssl-dev llvm pkg-config clang python3-dev
```
The `python3-dev` package is needed only for the Python bindings job. The
benchmark jobs need the same set minus `python3-dev`.
The default crypto backend is **OpenSSL** (`libssl-dev`). To build with the
NSS backend instead, install the NSS development libraries and pass
`--no-default-features --features nss` to Cargo:
```bash
# Debian/Ubuntu
apt-get install libnss3-dev
cargo build --no-default-features --features nss -p synta-ffi
```
---
## Running CI Locally
`contrib/ci/local-ci.sh` mirrors the GitHub Actions workflow exactly. Every
CI job is implemented in the script and can be run individually or together.
### Quick start
```bash
# Run all jobs in canonical order
./contrib/ci/local-ci.sh all
# List all available job names
./contrib/ci/local-ci.sh --list
```
### Selected jobs
Run only the checks relevant to your change:
```bash
# Formatting and linting
./contrib/ci/local-ci.sh fmt clippy
# Core tests (implies build)
./contrib/ci/local-ci.sh test
# C FFI tests (implies build)
./contrib/ci/local-ci.sh c-test
# Python bindings (implies build)
./contrib/ci/local-ci.sh python-test
# Markdown TOC check
./contrib/ci/local-ci.sh toc
# Update stale TOCs
./contrib/ci/local-ci.sh --update toc
# Validate C/C++ code samples in docs
./contrib/ci/local-ci.sh doc-c
# Validate Rust code samples in docs (with serde)
./contrib/ci/local-ci.sh doc-rust
# Memory-check C and Rust tests with Valgrind
./contrib/ci/local-ci.sh --valgrind c-test test
```
### Isolated build directory
To keep CI build artefacts separate from your incremental build:
```bash
CARGO_TARGET_DIR=/tmp/synta-target ./contrib/ci/local-ci.sh all
```
### CI job reference
| `build` | Compiles the full workspace (debug + release) with the default openssl backend; dual-backend crates (`synta-ffi`, `synta-python`, `synta-x509-verification`) are built separately to avoid linking both backends simultaneously; produces `libcsynta.so` and `include/synta.h` | — |
| `fmt` | Rust formatting (`cargo fmt --all -- --check`) | — |
| `ruff` | Python style (`ruff check` + `ruff format --check`) | — |
| `toc` | Markdown TOC blocks are up to date | — |
| `doc-python` | Python doc-sample syntax (`python3 -m py_compile`) | — |
| `clippy` | Clippy with `-D warnings` | `build` |
| `doc` | Rust API docs with `-D warnings` | `build` |
| `doc-c` | C/C++ doc samples compile with `-fsyntax-only` | `build` |
| `doc-rust` | Rust doc samples type-check via `cargo check` | `build` |
| `python-test` | Python binding tests (`pytest`) | `build` |
| `c-test` | C FFI tests (`make -C tests/c test`) | `build` |
| `test-codegen` | `synta-codegen` unit tests | `build` |
| `test-certificate` | `synta-certificate` tests | `build` |
| `test-krb5` | `synta-krb5` tests including build.rs codegen | `build` |
| `test-serde` | `serde` feature in isolation | `build` |
| `bench` | Benchmark suite compiles without errors (`--no-run`) | `build` |
| `test` | Full workspace tests on stable, beta, and nightly | `build` |
The `build` job runs first; all jobs that depend on it fan out in parallel on
GitHub Actions. Locally, the script runs them sequentially and auto-triggers
prerequisites when needed.
See `contrib/ci/README.md` for a complete reference including flags
(`--valgrind`, `--update`, `--show-results`, `--no-run`, `--no-deps`) and
benchmark-specific environment variables.
---
## Workspace Structure
The repository is a Cargo workspace with fifteen crates:
| `synta` | `.` | Core ASN.1 DER/BER/CER parser, encoder, and type system. Zero-copy where possible. |
| `synta-derive` | `synta-derive/` | Procedural macros (`#[derive(Asn1Sequence)]`, `#[derive(Asn1Set)]`, `#[derive(Asn1Choice)]`, `#[derive(Tagged)]`) for deriving `Encode` and `Decode` on Rust structs and enums. |
| `synta-codegen` | `synta-codegen/` | Library and CLI tool that parses ASN.1 schema files and generates Rust source code using `synta` and `synta-derive`. |
| `synta-certificate` | `synta-certificate/` | High-level X.509 certificate parser and PKI toolkit built on top of `synta`. Handles certificate field access, extension builders, CRL/CSR/OCSP structures, and X.509 path validation support (RFC 5280). |
| `synta-krb5` | `synta-krb5/` | Kerberos V5 ASN.1 types derived from RFC 4120, RFC 4121, RFC 4178, RFC 6113, and related RFCs, generated via `synta-codegen`. |
| `synta-mtc` | `synta-mtc/` | Merkle Tree Certificate (MTC) implementation including builder, validator, and crypto primitives per the IETF PLANTS draft. |
| `synta-x509-verification` | `synta-x509-verification/` | RFC 5280 X.509 certificate path validation (crypto-agnostic — delegates signature verification to a caller-supplied backend). |
| `synta-ffi` | `synta-ffi/` | C/C++ FFI library (`libcsynta`). The C header `include/synta.h` is auto-generated by cbindgen during the build. |
| `synta-tools` | `synta-tools/` | CLI utilities (`synta-tool`) for X.509 and MTC certificate inspection, including MTC standalone/landmark display and inclusion proof verification. |
| `synta-python` | `synta-python/` | PyO3-based Python extension module. Compiled by [maturin](https://www.maturin.rs/); installed as `synta._synta`. Exposes decoding, encoding, and certificate parsing to Python 3.8+. |
| `synta-python-common` | `synta-python-common/` | Shared utilities used by multiple PyO3 extension crates (error mapping, OID helpers). |
| `synta-python-krb5` | `synta-python-krb5/` | PyO3 extension for Kerberos V5 and SPNEGO types (`synta._krb5`). |
| `synta-python-mtc` | `synta-python-mtc/` | PyO3 extension for Merkle Tree Certificate types (`synta._mtc`). |
| `synta-bench` | `synta-bench/` | Criterion-based benchmarks comparing synta against x509-parser, x509-cert, OpenSSL, and the cryptography Python package. Also covers C FFI and Python binding overhead, and CA root-store parsing (Mozilla NSS, CCADB). |
| `synta-fuzz` | `synta-fuzz/` | Structured ASN.1 fuzzer for exercising the parser and encoder with random and seeded inputs. |
When adding a new feature, consider which crate it belongs in. Changes to
the core parser belong in `synta`; new high-level helpers for a specific
protocol may warrant a new crate (follow the `synta-krb5` or `synta-mtc`
pattern).
---
## Writing Tests
### Rust unit and integration tests
Tests live alongside the code they test in `#[cfg(test)]` modules, or as
integration tests in `tests/` at the crate root.
Run tests for a specific crate:
```bash
cargo test -p synta
cargo test -p synta-certificate
cargo test -p synta-codegen --all-features
```
Run the full workspace test suite (matches what CI runs):
```bash
cargo test --workspace --all-features
```
DER/BER test vectors live under `tests/vectors/`. When adding a new vector:
- Add the raw binary file under the appropriate subdirectory.
- Add a test that decodes it and asserts the expected field values.
- If the vector came from a third-party source, document its origin and
licence in `tests/vectors/README.md`.
### C FFI tests
C tests live in `tests/c/`. Each test is a small C program that links
against `libcsynta`.
Build and run the C tests:
```bash
cargo build --release -p synta-ffi
make -C tests/c test
```
To check for memory errors:
```bash
make -C tests/c valgrind
```
### Python tests
Python binding tests live in `tests/python/`. They require the `synta`
extension to be compiled first:
```bash
uv venv
uv pip install maturin pytest
uv run maturin develop
uv run pytest tests/python/ -v
```
---
## Documentation
### Rust API docs
Public items must have `///` doc comments. Code examples in doc comments
should be self-contained and runnable:
```rust,ignore
/// Decodes a DER-encoded integer.
///
/// # Example
///
/// ```rust
/// use synta::{Decoder, Encoding};
///
/// let data = [0x02, 0x01, 0x2a];
/// let mut decoder = Decoder::new(&data, Encoding::Der);
/// let n = decoder.decode_integer().unwrap();
/// assert_eq!(i64::try_from(n).unwrap(), 42);
/// ```
pub fn decode_integer(&mut self) -> Result<Integer> { ... }
```
The `doc-rust` CI job compiles every Rust code block in the `docs/` directory
as well as in doc comments. If an example is intentionally incomplete, mark
it `no_run` or `ignore`:
````markdown
```rust,no_run
// This example requires a network connection
```
````
### Markdown docs
Narrative documentation lives in `docs/`. After adding or renaming headings,
regenerate the table of contents:
```bash
./contrib/ci/local-ci.sh --update toc
```
The `toc` CI job verifies that every workspace Markdown file has an
up-to-date TOC block and fails the build if any are stale.
### Code samples in docs
C and C++ code blocks in Markdown files are compiled with `-fsyntax-only` by
the `doc-c` CI job. Python code blocks are syntax-checked with
`python3 -m py_compile`. Keep examples realistic and compilable; see
`contrib/validation/README.md` for how blocks are classified and when they
are skipped automatically.
---
## Benchmarks
The benchmark suite lives in `synta-bench/benches/`. Before running
benchmarks, ensure you have a release build:
```bash
cargo build --release -p synta-ffi
```
Run the library comparison benchmarks:
```bash
./contrib/ci/local-ci.sh --show-results bench-compare
```
Run the CA root-store benchmarks (downloads ~20 MB on first run):
```bash
./contrib/ci/local-ci.sh --show-results bench-ca-roots
```
Display results from a previous run without re-running:
```bash
./contrib/ci/local-ci.sh --no-run --show-results bench-compare
```
When contributing a performance improvement:
1. Run the relevant benchmarks before and after your change on the same
machine under the same conditions.
2. Include the benchmark output (or a summary) in your PR description.
3. Ensure the `bench` CI job still compiles without errors.
See `synta-bench/README.md` and `contrib/ci/README.md` for a full description
of benchmark flags, environment variables, and result interpretation.