Skip to main content

synta_codegen/
lib.rs

1//! ASN.1 schema parser and Rust code generator
2//!
3//! This library parses ASN.1 module definitions and generates Rust code
4//! using the `synta` library's derive macros.
5//!
6//! # Quick start
7//!
8//! ```no_run
9//! use synta_codegen::{parse, generate};
10//!
11//! let schema = r#"
12//!     Certificate DEFINITIONS ::= BEGIN
13//!         Certificate ::= SEQUENCE {
14//!             version INTEGER,
15//!             serialNumber INTEGER
16//!         }
17//!     END
18//! "#;
19//!
20//! let module = parse(schema).unwrap();
21//! let rust_code = generate(&module).unwrap();
22//! println!("{}", rust_code);
23//! ```
24//!
25//! # Configuration
26//!
27//! [`generate_with_config`] accepts a [`CodeGenConfig`] that controls several
28//! aspects of the emitted code.
29//!
30//! ## Owned vs. borrowed string types
31//!
32//! By default all ASN.1 string and binary types (`OCTET STRING`, `BIT STRING`,
33//! `UTF8String`, `PrintableString`, `IA5String`) are generated as **owned**
34//! heap-allocating types (`OctetString`, `BitString`, …).  This is convenient
35//! when constructing structs programmatically.
36//!
37//! For parse-only workloads (e.g. X.509 certificate inspection) you can switch
38//! to **zero-copy borrowed** types (`OctetStringRef<'a>`, `BitStringRef<'a>`,
39//! …) that borrow directly from the input buffer.  Structs that contain these
40//! fields automatically gain a `'a` lifetime parameter.
41//!
42//! ```no_run
43//! use synta_codegen::{parse, generate_with_config, CodeGenConfig, StringTypeMode};
44//!
45//! let schema = r#"
46//!     Msg DEFINITIONS ::= BEGIN
47//!         Msg ::= SEQUENCE {
48//!             payload OCTET STRING,
49//!             label   UTF8String
50//!         }
51//!     END
52//! "#;
53//!
54//! let module = parse(schema).unwrap();
55//! let config = CodeGenConfig {
56//!     string_type_mode: StringTypeMode::Borrowed,
57//!     ..Default::default()
58//! };
59//! // Emits:
60//! //   pub struct Msg<'a> {
61//! //       pub payload: OctetStringRef<'a>,
62//! //       pub label: Utf8StringRef<'a>,
63//! //   }
64//! let rust_code = generate_with_config(&module, config).unwrap();
65//! println!("{}", rust_code);
66//! ```
67//!
68//! String types that have no zero-copy variant (`TeletexString`, `BmpString`,
69//! `UniversalString`, `GeneralString`, `NumericString`, `VisibleString`) are
70//! always emitted as owned types regardless of [`StringTypeMode`].
71//!
72//! Named bit strings (`BIT STRING { flag(0), … }`) are always emitted as
73//! owned `BitString` because they are decoded into a concrete bit-field type.
74//!
75//! ## Derive macro gating
76//!
77//! By default every `Asn1Sequence` / `Asn1Set` / `Asn1Choice` derive and its
78//! associated `asn1(…)` helper attributes are wrapped in
79//! `#[cfg_attr(feature = "derive", …)]`.  This lets the consuming crate make
80//! `synta-derive` an **optional** dependency controlled by a Cargo feature:
81//!
82//! ```toml
83//! # Cargo.toml of the consuming crate (default behaviour)
84//! [dependencies]
85//! synta-derive = { version = "0.1", optional = true }
86//!
87//! [features]
88//! derive = ["dep:synta-derive"]
89//! ```
90//!
91//! Third-party crates that **always** depend on `synta-derive` and do not want
92//! to expose a `derive` Cargo feature can use [`DeriveMode::Always`]:
93//!
94//! ```no_run
95//! use synta_codegen::{parse, generate_with_config, CodeGenConfig, DeriveMode};
96//!
97//! let schema = r#"
98//!     Msg DEFINITIONS ::= BEGIN
99//!         Msg ::= SEQUENCE { id INTEGER }
100//!     END
101//! "#;
102//!
103//! let module = parse(schema).unwrap();
104//! let config = CodeGenConfig {
105//!     derive_mode: DeriveMode::Always,
106//!     ..Default::default()
107//! };
108//! // Emits:
109//! //   #[derive(Debug, Clone, PartialEq)]
110//! //   #[derive(Asn1Sequence)]          ← no cfg_attr wrapper
111//! //   pub struct Msg { pub id: Integer }
112//! let rust_code = generate_with_config(&module, config).unwrap();
113//! println!("{}", rust_code);
114//! ```
115//!
116//! If the crate uses a feature name other than `"derive"`, pass it via
117//! [`DeriveMode::Custom`]:
118//!
119//! ```no_run
120//! use synta_codegen::{parse, generate_with_config, CodeGenConfig, DeriveMode};
121//!
122//! # let schema = "Msg DEFINITIONS ::= BEGIN Msg ::= SEQUENCE { id INTEGER } END";
123//! # let module = parse(schema).unwrap();
124//! let config = CodeGenConfig {
125//!     derive_mode: DeriveMode::Custom("asn1-derive".to_string()),
126//!     ..Default::default()
127//! };
128//! // Emits:
129//! //   #[cfg_attr(feature = "asn1-derive", derive(Asn1Sequence))]
130//! let rust_code = generate_with_config(&module, config).unwrap();
131//! println!("{}", rust_code);
132//! ```
133//!
134//! ## Import path prefix
135//!
136//! Use [`CodeGenConfig::with_crate_imports`], [`CodeGenConfig::with_super_imports`],
137//! or [`CodeGenConfig::with_custom_prefix`] to emit `use` statements instead of
138//! the default comment-only import annotations.
139//!
140//! ## Constrained INTEGER type selection
141//!
142//! When a top-level `INTEGER` type carries a value-range constraint (e.g.
143//! `INTEGER (0..100)`), synta-codegen generates a newtype whose inner field is
144//! the **smallest native Rust integer primitive** that covers the declared range,
145//! rather than the arbitrary-precision `Integer` type:
146//!
147//! - Lower bound ≥ 0 → unsigned: `u8` (≤255), `u16` (≤65535), `u32` (≤4294967295), `u64`.
148//! - Lower bound < 0 → signed: `i8`, `i16`, `i32`, `i64`.
149//! - Unconstrained bounds (`MIN`/`MAX`, named values) → `i64`.
150//!
151//! Using a primitive type means the generated struct automatically derives
152//! `Copy`, `PartialOrd`, and `Ord`, and avoids heap allocation.  For example,
153//! `Percentage ::= INTEGER (0..100)` produces:
154//!
155//! ```text
156//! #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
157//! pub struct Percentage(u8);
158//!
159//! impl Percentage {
160//!     pub fn new(value: u8) -> Result<Self, &'static str> { ... }
161//!     pub const fn new_unchecked(value: u8) -> Self { Percentage(value) }
162//!     pub const fn get(&self) -> u8 { self.0 }
163//!     pub fn into_inner(self) -> u8 { self.0 }
164//! }
165//! ```
166//!
167//! The equivalent C generation uses `uint8_t` / `uint16_t` / `uint32_t` /
168//! `uint64_t` for non-negative ranges and `int8_t` / `int16_t` / `int32_t` /
169//! `int64_t` for signed ranges.
170
171pub mod ast;
172pub mod c_cmake_codegen;
173pub mod c_codegen;
174pub mod c_impl_codegen;
175pub mod c_meson_codegen;
176pub mod codegen;
177pub mod import_graph;
178pub mod naming;
179pub mod parser;
180
181pub use ast::{Definition, Module, Type};
182pub use c_cmake_codegen::{generate_cmake, CMakeConfig};
183pub use c_codegen::{generate_c, generate_c_with_config, CCodeGenConfig};
184pub use c_impl_codegen::{generate_c_impl, CImplConfig, PatternMode};
185pub use c_meson_codegen::{generate_meson, MesonConfig};
186pub use codegen::{generate, generate_with_config, CodeGenConfig, DeriveMode, StringTypeMode};
187pub use import_graph::{detect_cycles, topological_order, ImportCycle};
188pub use naming::module_file_stem;
189pub use parser::{parse, ParseError};
190
191/// Locate the `asn1/` schema directory containing the ASN.1 schemas.
192///
193/// Call this from a build script (`build.rs`) to obtain the path to the shared
194/// ASN.1 schema files that ship with the `synta` package.  Three layouts are
195/// checked in order:
196///
197/// 1. **Crate-local** — `<CARGO_MANIFEST_DIR>/asn1/` exists inside the calling
198///    crate itself.  Handles self-contained crates that bundle their own schemas.
199///
200/// 2. **Workspace build** — the calling crate sits one level below the
201///    workspace root where `asn1/` lives.  `../asn1` relative to
202///    `CARGO_MANIFEST_DIR` is returned.
203///
204/// 3. **crates.io / registry build** — falls back to `cargo metadata` to find
205///    the source location of the `synta` package and returns its `asn1/`
206///    subdirectory.  The `synta` package is identified by its manifest
207///    directory name: `"synta"` (workspace root) or `"synta-X.Y.Z"` (crates.io
208///    registry entry, recognised by `"synta-"` followed by an ASCII digit).
209///
210/// # Panics
211///
212/// Panics if none of the three layouts yields a valid `asn1/` directory.
213pub fn find_asn1_dir() -> std::path::PathBuf {
214    let manifest_dir =
215        std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect(
216            "CARGO_MANIFEST_DIR not set — find_asn1_dir() must be called from a build script",
217        ));
218
219    // 1. Crate-local: asn1/ ships inside the calling crate itself.
220    let local = manifest_dir.join("asn1");
221    if local.is_dir() {
222        return local;
223    }
224
225    // 2. Workspace: asn1/ is one level above the calling crate.
226    let workspace = manifest_dir.join("../asn1");
227    if workspace.is_dir() {
228        return workspace;
229    }
230
231    // 3. crates.io / registry: use `cargo metadata` to locate the synta
232    //    package and return its bundled asn1/ directory.
233    let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
234    if let Some(asn1) = find_asn1_via_cargo_metadata(&cargo, &manifest_dir) {
235        return asn1;
236    }
237
238    panic!(
239        "Cannot locate the synta asn1/ schema directory.\n\
240         Tried: {m}/asn1 (crate-local), ../asn1 (workspace), \
241         and `cargo metadata` (crates.io).\n\
242         CARGO_MANIFEST_DIR = {m:?}",
243        m = manifest_dir.display()
244    );
245}
246
247/// Use `cargo metadata` to find the `asn1/` directory inside the `synta`
248/// package source tree.
249///
250/// Scans every `manifest_path` value in the metadata JSON and identifies the
251/// `synta` package by the name of its containing directory: `"synta"` (when
252/// the workspace root happens to be in a directory named `synta`) or
253/// `"synta-X.Y.Z"` (the standard crates.io registry layout).
254fn find_asn1_via_cargo_metadata(
255    cargo: &str,
256    manifest_dir: &std::path::Path,
257) -> Option<std::path::PathBuf> {
258    let output = std::process::Command::new(cargo)
259        .args(["metadata", "--format-version=1", "--manifest-path"])
260        .arg(manifest_dir.join("Cargo.toml"))
261        .output()
262        .ok()?;
263
264    if !output.status.success() {
265        return None;
266    }
267
268    let json = String::from_utf8(output.stdout).ok()?;
269
270    // Cargo emits compact JSON; each manifest_path value appears as:
271    //   "manifest_path":"/absolute/path/to/Cargo.toml"
272    // We scan every occurrence and select the one whose parent directory is
273    // named "synta" or "synta-<version>" (digit after the hyphen).
274    let mp_prefix = r#""manifest_path":""#;
275    let mut rest = json.as_str();
276    while let Some(pos) = rest.find(mp_prefix) {
277        let after = &rest[pos + mp_prefix.len()..];
278        if let Some(end) = after.find('"') {
279            // Unescape JSON backslash sequences (relevant on Windows).
280            let path_str = after[..end].replace("\\\\", "\\").replace("\\/", "/");
281            let manifest = std::path::PathBuf::from(&path_str);
282            if let Some(dir) = manifest.parent() {
283                let dir_name = dir.file_name().unwrap_or_default().to_string_lossy();
284                let is_synta = dir_name == "synta"
285                    || (dir_name.starts_with("synta-")
286                        && dir_name["synta-".len()..]
287                            .chars()
288                            .next()
289                            .is_some_and(|c| c.is_ascii_digit()));
290                if is_synta {
291                    let asn1 = dir.join("asn1");
292                    if asn1.is_dir() {
293                        return Some(asn1);
294                    }
295                }
296            }
297            rest = &rest[pos + mp_prefix.len() + end + 1..];
298        } else {
299            break;
300        }
301    }
302
303    None
304}
305
306/// Parse ASN.1 schema and generate Rust code in one step
307pub fn parse_and_generate(input: &str) -> Result<String, Box<dyn std::error::Error>> {
308    let module = parse(input)?;
309    let code = generate(&module)?;
310    Ok(code)
311}
312
313/// Parse ASN.1 schema and generate C header in one step
314pub fn parse_and_generate_c(input: &str) -> Result<String, Box<dyn std::error::Error>> {
315    let module = parse(input)?;
316    let code = generate_c(&module)?;
317    Ok(code)
318}
319
320/// Parse ASN.1 schema and generate C implementation in one step
321pub fn parse_and_generate_c_impl(
322    input: &str,
323    header_file: &str,
324) -> Result<String, Box<dyn std::error::Error>> {
325    let module = parse(input)?;
326    let config = CImplConfig {
327        header_file: header_file.to_string(),
328        arena_mode: false,
329        pattern_mode: Default::default(),
330        with_containing: false,
331    };
332    let code = generate_c_impl(&module, config)?;
333    Ok(code)
334}