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}