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.
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:
// SPDX-License-Identifier: MIT OR Apache-2.0Synta uses the Developer Certificate of Origin (DCO)
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:
git commit -sThis 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 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 -scertifies 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.
The canonical repository is at
codeberg.org/abbra/synta.
Pull requests should target the main branch.
Fork the repository on Codeberg, then clone your fork:
git clone ssh://git@codeberg.org/<your-username>/synta.git
cd synta
git remote add upstream https://codeberg.org/abbra/synta.gitAlways work on a dedicated branch, not directly on main:
git fetch upstream
git checkout -b my-feature upstream/mainChoose a short, descriptive branch name (fix-ber-length-decoding,
add-generalizedtime-serde, krb5-pa-data-types, etc.).
Implement your change and add tests (see Writing Tests). Keep each commit focused on a single concern; avoid bundling unrelated fixes in the same PR.
Run the full CI suite before pushing to catch problems early:
./contrib/ci/local-ci.sh allSee Running CI Locally for details on individual jobs and flags.
Use git commit -s to sign off each commit. Follow the
commit message conventions described below.
Push your branch and open a pull request against abbra/synta:main:
git push origin my-featureThen open a PR on Codeberg. In the PR description:
- Summarise what the change does and why.
- Reference any related issues with
Fixes #NNNorSee #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.
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.
Synta uses Conventional Commits 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).
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:
git config commit.template .gitmessageAfter 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:
| Type | When to use |
|---|---|
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").
Formatting is enforced by rustfmt with the project defaults (no
rustfmt.toml). Run before committing:
cargo fmt --allCI fails if any file would be reformatted.
Linting is enforced by Clippy with all warnings treated as errors:
cargo clippy --workspace -- -D warningsFix 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:
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-depsPublic 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 Traitin argument position over generic bounds for readability when there is only one trait bound. - Error types should implement
std::error::Errorand be non-exhaustive (#[non_exhaustive]) when public.
Python files in contrib/ and python/ are linted with
Ruff:
ruff check contrib/
ruff format --check contrib/Fix issues before committing. CI runs Ruff via uv when available.
Every Markdown file must have an up-to-date doctoc-compatible table of contents block (see Markdown docs). After editing a document, regenerate its TOC:
./contrib/ci/local-ci.sh --update toc| Tool | Notes |
|---|---|
| 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. 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. |
| Tool | Notes |
|---|---|
uv |
Recommended for managing Python environments. Install from 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. |
The C-based jobs require the following native libraries. On Debian/Ubuntu:
apt-get install build-essential libssl-dev llvm pkg-config clang python3-devThe 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:
# Debian/Ubuntu
apt-get install libnss3-dev
cargo build --no-default-features --features nss -p synta-fficontrib/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.
# Run all jobs in canonical order
./contrib/ci/local-ci.sh all
# List all available job names
./contrib/ci/local-ci.sh --listRun only the checks relevant to your change:
# 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 testTo keep CI build artefacts separate from your incremental build:
CARGO_TARGET_DIR=/tmp/synta-target ./contrib/ci/local-ci.sh all| Job | What it checks | Prerequisites |
|---|---|---|
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.
The repository is a Cargo workspace with fifteen crates:
| Crate | Path | Purpose |
|---|---|---|
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; 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).
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:
cargo test -p synta
cargo test -p synta-certificate
cargo test -p synta-codegen --all-featuresRun the full workspace test suite (matches what CI runs):
cargo test --workspace --all-featuresDER/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 tests live in tests/c/. Each test is a small C program that links
against libcsynta.
Build and run the C tests:
cargo build --release -p synta-ffi
make -C tests/c testTo check for memory errors:
make -C tests/c valgrindPython binding tests live in tests/python/. They require the synta
extension to be compiled first:
uv venv
uv pip install maturin pytest
uv run maturin develop
uv run pytest tests/python/ -vPublic items must have /// doc comments. Code examples in doc comments
should be self-contained and runnable:
/// 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:
```rust,no_run
// This example requires a network connection
```Narrative documentation lives in docs/. After adding or renaming headings,
regenerate the table of contents:
./contrib/ci/local-ci.sh --update tocThe toc CI job verifies that every workspace Markdown file has an
up-to-date TOC block and fails the build if any are stale.
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.
The benchmark suite lives in synta-bench/benches/. Before running
benchmarks, ensure you have a release build:
cargo build --release -p synta-ffiRun the library comparison benchmarks:
./contrib/ci/local-ci.sh --show-results bench-compareRun the CA root-store benchmarks (downloads ~20 MB on first run):
./contrib/ci/local-ci.sh --show-results bench-ca-rootsDisplay results from a previous run without re-running:
./contrib/ci/local-ci.sh --no-run --show-results bench-compareWhen contributing a performance improvement:
- Run the relevant benchmarks before and after your change on the same machine under the same conditions.
- Include the benchmark output (or a summary) in your PR description.
- Ensure the
benchCI 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.