synta 0.1.1

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
# 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

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*

- [Licensing and Sign-off]#licensing-and-sign-off
  - [Dual licence]#dual-licence
  - [Developer Certificate of Origin]#developer-certificate-of-origin
  - [AI-assisted contributions]#ai-assisted-contributions
- [Contribution Workflow]#contribution-workflow
  - [1. Fork and clone]#1-fork-and-clone
  - [2. Create a branch]#2-create-a-branch
  - [3. Make your changes]#3-make-your-changes
  - [4. Run local CI]#4-run-local-ci
  - [5. Commit]#5-commit
  - [6. Open a pull request]#6-open-a-pull-request
  - [7. Review and merge]#7-review-and-merge
- [Commit Message Conventions]#commit-message-conventions
- [Code Style]#code-style
  - [Rust]#rust
  - [Python]#python
  - [Markdown]#markdown
- [Development Environment]#development-environment
  - [Required tools]#required-tools
  - [Optional tools]#optional-tools
  - [System libraries]#system-libraries
- [Running CI Locally]#running-ci-locally
  - [Quick start]#quick-start
  - [Selected jobs]#selected-jobs
  - [Isolated build directory]#isolated-build-directory
  - [CI job reference]#ci-job-reference
- [Workspace Structure]#workspace-structure
- [Writing Tests]#writing-tests
  - [Rust unit and integration tests]#rust-unit-and-integration-tests
  - [C FFI tests]#c-ffi-tests
  - [Python tests]#python-tests
- [Documentation]#documentation
  - [Rust API docs]#rust-api-docs
  - [Markdown docs]#markdown-docs
  - [Code samples in docs]#code-samples-in-docs
- [Benchmarks]#benchmarks

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

---

## 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:**

| 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").

---

## 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

| 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]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

| Tool | Notes |
|---|---|
| `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`.

---

## 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

| Job | What it checks | Prerequisites |
|---|---|---|
| `build` | Compiles the full workspace (debug + release); 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 eight 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 built on top of `synta`. Handles algorithm identification and certificate field access (RFC 5280). |
| `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-ffi` | `synta-ffi/` | C/C++ FFI library (`libcsynta`). The C header `include/synta.h` is auto-generated by cbindgen during the build. |
| `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-krb5` | `synta-krb5/` | Kerberos V5 ASN.1 types derived from RFC 4120, RFC 4121, RFC 4178, and RFC 6113, generated via `synta-codegen`. |

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` 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.