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
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
# Rust Code Generation

<!-- 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)*

- [1. Quick Start]#1-quick-start
  - [CLI]#cli
  - [Library API]#library-api
- [2. Type Mappings]#2-type-mappings
- [3. Naming Conventions]#3-naming-conventions
  - [Type Names -- PascalCase]#type-names-pascalcase
  - [Field Names -- snake_case]#field-names-snake_case
  - [Named Value Constants -- SCREAMING_SNAKE_CASE]#named-value-constants-screamingsnakecase
  - [Module Names in Use Statements]#module-names-in-use-statements
- [4. Constraint Validation]#4-constraint-validation
  - [Value Range Constraints]#value-range-constraints
  - [Size Constraints]#size-constraints
  - [Permitted Alphabet Constraints (FROM)]#permitted-alphabet-constraints-from
  - [PATTERN Constraints]#pattern-constraints
  - [CONTAINING Constraints]#containing-constraints
  - [Union, Intersection, and Complement Constraints]#union-intersection-and-complement-constraints
  - [Named Bits]#named-bits
  - [Inner Type Constraints]#inner-type-constraints
  - [Additional Methods on Constrained Newtypes]#additional-methods-on-constrained-newtypes
- [5. OPTIONAL and DEFAULT Handling]#5-optional-and-default-handling
  - [OPTIONAL Fields]#optional-fields
  - [DEFAULT Values]#default-values
- [6. Tagged Fields]#6-tagged-fields
  - [Attribute syntax]#attribute-syntax
  - [Example]#example
- [7. IMPORTS and Module References]#7-imports-and-module-references
  - [No Import Prefix (default)]#no-import-prefix-default
  - [--crate-imports]#-crate-imports
  - [--super-imports]#-super-imports
  - [--module-prefix]#-module-prefix
  - [Multiple import sources]#multiple-import-sources
- [8. no_std Support]#8-no_std-support
- [9. build.rs Integration]#9-buildrs-integration
  - [Multi-module build.rs]#multi-module-buildrs
  - [Recommended project layout]#recommended-project-layout
- [10. Library API Reference]#10-library-api-reference
  - [`parse`]#parse
  - [`generate`]#generate
  - [`generate_with_config`]#generatewithconfig
  - [`CodeGenConfig`]#codegenconfig
  - [`StringTypeMode`]#stringtypemode
  - [`DeriveMode`]#derivemode
- [11. Owned vs. Borrowed String Types]#11-owned-vs-borrowed-string-types
  - [Default: Owned types]#default-owned-types
  - [Borrowed mode: zero-copy Ref types]#borrowed-mode-zero-copy-ref-types
  - [Which types are affected]#which-types-are-affected
  - [Lifetime propagation]#lifetime-propagation
  - [Named bit strings are always owned]#named-bit-strings-are-always-owned
- [What Is Not Generated]#what-is-not-generated

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

This document describes how synta-codegen generates Rust source code from ASN.1 module
definitions. It covers type mappings, naming conventions, constraint validation, OPTIONAL
and DEFAULT handling, tagged fields, import modes, no_std support, build.rs integration,
and the library API.

---

## 1. Quick Start

### CLI

Generate Rust from an ASN.1 schema file:

```sh
synta-codegen --lang rust schema.asn1 -o src/generated.rs
```

`--lang rust` is the default and may be omitted.

Generate into a directory (one file per ASN.1 module):

```sh
synta-codegen schema.asn1 --output-dir src/gen/
```

Import mode flags (select one):

```sh
synta-codegen schema.asn1 --crate-imports   # use crate::module::Type
synta-codegen schema.asn1 --super-imports   # use super::module::Type
synta-codegen schema.asn1 --module-prefix my_crate  # use my_crate::module::Type
```

For no_std environments:

```sh
synta-codegen schema.asn1 --use-core -o src/generated.rs
```

### Library API

```rust
use synta_codegen::{parse, generate, generate_with_config, CodeGenConfig};

let src = std::fs::read_to_string("schema.asn1")?;
let module = parse(&src)?;

// Defaults: std paths, no import prefix, owned string types
let rust_code = generate(&module)?;

// With crate-relative imports
let rust_code = generate_with_config(&module, CodeGenConfig::with_crate_imports())?;

// With zero-copy borrowed string types (for parse-only workloads)
use synta_codegen::StringTypeMode;
let config = CodeGenConfig {
    string_type_mode: StringTypeMode::Borrowed,
    ..Default::default()
};
let rust_code = generate_with_config(&module, config)?;
```

---

## 2. Type Mappings

The table below lists every ASN.1 built-in type and the Rust type or structure that
synta-codegen emits.

| ASN.1 Type            | Generated Rust (default / owned)                           | Borrowed mode (`StringTypeMode::Borrowed`) |
|-----------------------|------------------------------------------------------------|--------------------------------------------|
| SEQUENCE              | `struct` with named `pub` fields                          ||
| SET                   | `struct` with named `pub` fields (same as SEQUENCE)       ||
| SEQUENCE OF T         | type alias `type Foo = Vec<T>` (SEQUENCE tag 0x30)        ||
| SET OF T              | type alias `type Foo = SetOf<T>` (SET tag 0x31)           ||
| CHOICE                | `enum` with one variant per alternative                    ||
| INTEGER               | newtype wrapping `synta::Integer`                         ||
| INTEGER (constrained) | newtype wrapping a native primitive (`u8`/`i8``u64`/`i64`); range-checked `new()` ||
| ENUMERATED            | `enum` with `i64` discriminant variants                   ||
| BOOLEAN               | `synta::Boolean`                                          ||
| OCTET STRING          | `synta::OctetString`                                      | `synta::OctetStringRef<'a>`                |
| OCTET STRING (const.) | newtype wrapping `synta::OctetString`                     | newtype wrapping `synta::OctetStringRef<'a>` |
| BIT STRING            | `synta::BitString`                                        | `synta::BitStringRef<'a>`                  |
| BIT STRING (named)    | `synta::BitString` (always owned; see §11)                | `synta::BitString` (always owned)          |
| BIT STRING (const.)   | newtype wrapping `synta::BitString`                       | newtype wrapping `synta::BitStringRef<'a>` |
| OBJECT IDENTIFIER     | `synta::ObjectIdentifier`                                 ||
| NULL                  | `synta::Null`                                             ||
| REAL                  | `synta::Real`                                             ||
| UTF8String            | `synta::Utf8String`                                       | `synta::Utf8StringRef<'a>`                 |
| PrintableString       | `synta::PrintableString`                                  | `synta::PrintableStringRef<'a>`            |
| IA5String             | `synta::IA5String`                                        | `synta::IA5StringRef<'a>`                  |
| TeletexString         | `synta::TeletexString`                                    | `synta::TeletexString` (no Ref variant)    |
| UniversalString       | `synta::UniversalString`                                  | `synta::UniversalString` (no Ref variant)  |
| BMPString             | `synta::BmpString`                                        | `synta::BmpString` (no Ref variant)        |
| GeneralString         | `synta::GeneralString`                                    | `synta::GeneralString` (no Ref variant)    |
| NumericString         | `synta::NumericString`                                    | `synta::NumericString` (no Ref variant)    |
| VisibleString         | `synta::VisibleString`                                    | `synta::VisibleString` (no Ref variant)    |
| UTCTime               | `synta::UtcTime`                                          ||
| GeneralizedTime       | `synta::GeneralizedTime`                                  ||
| ANY                   | `synta::Element<'a>` (or `synta::RawDer<'a>` with `any_as_raw_der`) ||
| ANY DEFINED BY        | `synta::Element<'a>` (or `synta::RawDer<'a>` with `any_as_raw_der`) ||
| TypeRef (no extra constraint) | type alias `type Foo = Bar`                      ||
| TypeRef (extra constraint)    | newtype `struct Foo(pub Bar)` with checked `new()` ||

"—" means the type is unaffected by `StringTypeMode`.  See [§11](#11-owned-vs-borrowed-string-types) for details and examples.

---

## 3. Naming Conventions

Naming follows the rules in `naming.rs`. The three categories are type names, field
names, and named-value constants.

### Type Names -- PascalCase

Each hyphen-delimited segment is converted: first character uppercased, remaining
characters lowercased. All-caps heuristic applies per segment, not per character.

| ASN.1 name          | Rust name           |
|---------------------|---------------------|
| `Certificate`       | `Certificate`       |
| `KDC-REQ`           | `KdcReq`            |
| `TBSCertificate`    | `Tbscertificate`    |
| `my-Type`           | `MyType`            |
| `PA-DATA`           | `PaData`            |

### Field Names -- snake_case

Hyphens are replaced with underscores. Rust keywords receive an `r#` prefix.

| ASN.1 field name    | Rust field name     |
|---------------------|---------------------|
| `serialNumber`      | `serial_number`     |
| `issuer-unique-id`  | `issuer_unique_id`  |
| `type`              | `r#type`            |
| `mod`               | `r#mod`             |
| `version`           | `version`           |

### Named Value Constants -- SCREAMING_SNAKE_CASE

Named values in INTEGER or ENUMERATED types are placed as associated constants on the
generated type.

ASN.1:

```asn1
Protocol ::= INTEGER { tcp(6), udp(17) }
```

Generated Rust:

```rust
pub struct Protocol(pub synta::Integer);

impl Protocol {
    pub const TCP: i64 = 6;
    pub const UDP: i64 = 17;
}
```

### Module Names in Use Statements

ASN.1 module names are converted to snake_case when used in Rust `use` paths.
Each hyphen-separated segment is lowercased independently; camelCase boundaries
are split before each uppercase letter that follows a lowercase letter.

| ASN.1 module name    | Rust path segment      |
|----------------------|------------------------|
| `BaseTypes`          | `base_types`           |
| `UserModule`         | `user_module`          |
| `CamelCaseModule`    | `camel_case_module`    |
| `kebab-case-module`  | `kebab_case_module`    |
| `SCREAMING-CASE`     | `screaming_case`       |
| `PKIX1Explicit88`    | `p_k_i_x1_explicit88` |

The module name from the ASN.1 `DEFINITIONS` header is used, not the file name.

---

## 4. Constraint Validation

Constraints are checked inside a `new()` constructor on the generated newtype. The
constructor returns `Result<Self, &'static str>`.

### Value Range Constraints

ASN.1:

```asn1
Port ::= INTEGER (1..65535)
```

Generated Rust:

```rust
pub struct Port(u16);

impl Port {
    pub fn new(value: u16) -> std::result::Result<Self, &'static str> {
        let val: i64 = value as i64;
        if (1..=65535).contains(&val) {
            Ok(Self(value))
        } else {
            Err("must be in range 1..65535")
        }
    }
    pub const fn new_unchecked(value: u16) -> Self { Self(value) }
    pub const fn get(&self) -> u16 { self.0 }
    pub fn into_inner(self) -> u16 { self.0 }
}
```

The inner field type is the smallest unsigned or signed native Rust integer
that covers the declared range: `u8`/`u16`/`u32`/`u64` when the lower bound
is ≥ 0, or `i8`/`i16`/`i32`/`i64` when the lower bound is negative.  Because
the inner type is a primitive, the generated struct derives `Copy`, `PartialOrd`,
and `Ord` automatically.

### Size Constraints

ASN.1:

```asn1
Label ::= UTF8String (SIZE (1..64))
```

Generated Rust:

```rust
pub struct Label(pub synta::Utf8String);

impl Label {
    pub fn new(value: synta::Utf8String) -> std::result::Result<Self, &'static str> {
        let len = value.as_str().len();
        if !(1..=64).contains(&len) {
            return Err("Label: size out of range 1..64");
        }
        Ok(Self(value))
    }
}
```

### Permitted Alphabet Constraints (FROM)

ASN.1:

```asn1
PrintableText ::= PrintableString (FROM ("A".."Z" | "a".."z" | " "))
```

The `new()` constructor iterates over each character and checks membership in the
permitted set.

### PATTERN Constraints

When the `regex` feature is enabled, a static `Lazy<Regex>` is generated and matched
inside `new()`. When the feature is absent, a TODO comment is emitted instead.

With `regex` feature:

```rust,ignore
use once_cell::sync::Lazy;
use regex::Regex;

static FOO_PATTERN: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"^[A-Z]{2}$").expect("invalid regex")
});

impl Foo {
    pub fn new(value: synta::PrintableString) -> Result<Self, &'static str> {
        if !FOO_PATTERN.is_match(value.as_str()) {
            return Err("Foo: value does not match required pattern");
        }
        Ok(Self(value))
    }
}
```

Without `regex` feature:

```rust
pub struct Foo(pub synta::PrintableString);

impl Foo {
    pub fn new(value: synta::PrintableString) -> std::result::Result<Self, &'static str> {
        // TODO: pattern constraint ^[A-Z]{2}$ not validated (enable feature "regex")
        Ok(Self(value))
    }
}
```

### CONTAINING Constraints

When the `validate_containing` feature is enabled, the constructor decodes the inner
type from the raw value using a scratch decoder. When the feature is absent, a TODO
comment is emitted.

### Union, Intersection, and Complement Constraints

- Union (`|`): `new()` returns `Ok` if any one condition passes.
- Intersection (`^`): `new()` returns `Ok` only if all conditions pass.
- Complement (`ALL EXCEPT`): `new()` returns `Ok` if the inner constraint does NOT pass.

### Named Bits

ASN.1:

```asn1
KeyUsage ::= BIT STRING {
    digitalSignature (0),
    keyEncipherment  (2)
}
```

Generated Rust:

```rust
pub struct KeyUsage(pub synta::BitString);

impl KeyUsage {
    pub const DIGITAL_SIGNATURE: u32 = 0;
    pub const KEY_ENCIPHERMENT: u32 = 2;
}
```

### Inner Type Constraints

For `SEQUENCE OF` or `SET OF` with element constraints, `new()` iterates over elements
and validates each one.

ASN.1:

```asn1
PortList ::= SEQUENCE OF INTEGER (1..65535)
```

The generated `new()` on `PortList` checks that every element is within `1..65535`.

### Additional Methods on Constrained Newtypes

Every constrained newtype -- whether wrapping INTEGER, a string type, OCTET STRING,
or BIT STRING -- receives additional methods beyond `new()`.

**For constrained INTEGER:**

```rust
pub struct Port(u16);

impl Port {
    // Validated constructor; accepts the native primitive type
    pub fn new(value: u16) -> std::result::Result<Self, &'static str> {
        let val: i64 = value as i64;
        if (1..=65535).contains(&val) {
            Ok(Self(value))
        } else {
            Err("must be in range 1..65535")
        }
    }

    // Bypass validation -- use only for data already known to be valid
    pub const fn new_unchecked(value: u16) -> Self { Self(value) }

    // Return the inner value by copy (primitives are Copy)
    pub const fn get(&self) -> u16 { self.0 }

    // Consume into the inner value
    pub fn into_inner(self) -> u16 { self.0 }
}

// TryFrom<Integer> is generated for the decode path:
// wire Integer → as_i64 → range-check → native primitive
impl core::convert::TryFrom<Integer> for Port {
    type Error = &'static str;
    fn try_from(value: Integer) -> std::result::Result<Self, Self::Error> {
        let n = value.as_i64().map_err(|_| "integer value out of i64 range")?;
        let v = u16::try_from(n).map_err(|_| "must be in range 1..65535")?;
        Self::new(v)
    }
}
```

**For constrained string types (IA5String, PrintableString, Utf8String):**

```rust
pub struct Label(pub synta::Utf8String);

impl Label {
    pub fn new(value: synta::Utf8String) -> std::result::Result<Self, &'static str> {
        let len = value.as_str().len();
        if !(1..=64).contains(&len) {
            return Err("Label: size out of range 1..64");
        }
        Ok(Self(value))
    }
    pub fn new_unchecked(value: synta::Utf8String) -> Self { Self(value) }
    pub fn get(&self) -> &synta::Utf8String { &self.0 }
    pub fn as_str(&self) -> &str { self.0.as_str() }  // text string types only
    pub fn into_inner(self) -> synta::Utf8String { self.0 }
}

impl core::convert::TryFrom<synta::Utf8String> for Label {
    type Error = &'static str;
    fn try_from(value: synta::Utf8String) -> std::result::Result<Self, Self::Error> {
        Self::new(value)
    }
}
```

`as_str()` is generated for text string types (`IA5String`, `PrintableString`,
`Utf8String`). It is not generated for `OctetString` or `BitString` constrained
newtypes.

Note: `new_unchecked` and `get` on INTEGER constrained types are `const fn`; the
string equivalents are not.

---

## 5. OPTIONAL and DEFAULT Handling

### OPTIONAL Fields

Fields marked `OPTIONAL` in ASN.1 are generated as `Option<T>` in the Rust struct.
Field order matches the ASN.1 definition order.

ASN.1:

```asn1
TBSCertificate ::= SEQUENCE {
    version         [0] EXPLICIT INTEGER DEFAULT 0,
    serialNumber    INTEGER,
    issuerUniqueID  [1] IMPLICIT BIT STRING OPTIONAL
}
```

Generated Rust:

```rust
#[derive(Debug, Clone, PartialEq)]
pub struct Tbscertificate {
    pub version: Option<synta::Integer>,
    pub serial_number: synta::Integer,
    pub issuer_unique_id: Option<synta::BitString>,
}
```

### DEFAULT Values

When every field in a struct is either OPTIONAL or carries a DEFAULT value,
synta-codegen generates `impl Default` for the struct.

- DEFAULT integer literals -> `const` used as the default expression.
- DEFAULT boolean -> `true` or `false` literal.
- DEFAULT named values -> reference to the named constant on the type.

`impl Default` is NOT generated when some fields are mandatory and carry no default.

---

## 6. Tagged Fields

Context tags (`[N] EXPLICIT` and `[N] IMPLICIT`) are parsed and preserved in the ASN.1
AST. Actual encode and decode behavior is handled by the synta library's derive macros
(`Asn1Sequence`, `Asn1Choice`, `Asn1Set`). synta-codegen emits a `#[cfg_attr]` field
attribute that passes the tag number and mode to the derive macro.

### Attribute syntax

Every context-specific tag attribute must carry an explicit mode keyword.
Writing `asn1(tag(N))` without `implicit` or `explicit` is a compile error in
the Synta derive macros.  When the ASN.1 module declares a default tagging mode,
synta-codegen resolves that default at generation time and always emits one of the
two explicit forms:

```rust
#[cfg_attr(feature = "derive", asn1(tag(N, implicit)))]
pub field: T,

#[cfg_attr(feature = "derive", asn1(tag(N, explicit)))]
pub field: T,
```

### Example

ASN.1 (module default tagging: IMPLICIT):

```asn1
Message ::= SEQUENCE {
    priority   [0] INTEGER OPTIONAL,          -- inherits module default: IMPLICIT
    expires    [1] IMPLICIT GeneralizedTime OPTIONAL,
    metadata   [2] EXPLICIT OCTET STRING OPTIONAL
}
```

Generated Rust:

```rust
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "derive", derive(Asn1Sequence))]
pub struct Message {
    // [0] inherits the module default (IMPLICIT); codegen resolves it at generation time.
    #[cfg_attr(feature = "derive", asn1(tag(0, implicit)))]
    pub priority: Option<synta::Integer>,
    #[cfg_attr(feature = "derive", asn1(tag(1, implicit)))]
    pub expires: Option<synta::GeneralizedTime>,
    #[cfg_attr(feature = "derive", asn1(tag(2, explicit)))]
    pub metadata: Option<synta::OctetString>,
}
```

The `#[cfg_attr(feature = "derive", ...)]` guard means the attribute is active only when
the `derive` feature is enabled, keeping the generated types usable without the
`synta-derive` crate.

---

## 7. IMPORTS and Module References

ASN.1 IMPORTS are converted to Rust `use` statements. Three modes are available,
selected by CLI flag.

### No Import Prefix (default)

No `use` statements are emitted for imported types. Types are expected to be in scope
by other means (e.g., a hand-written `mod.rs`).

### --crate-imports

```rust
use crate::pkix::AlgorithmIdentifier;
use crate::pkix::Name;
```

Module name is the snake_case form of the ASN.1 module name from which the type is
imported.

### --super-imports

```rust
use super::pkix::AlgorithmIdentifier;
use super::pkix::Name;
```

Useful when the generated file lives inside a subdirectory module and the parent module
re-exports the dependencies.

### --module-prefix

```sh
synta-codegen schema.asn1 --module-prefix my_crate
```

Generates:

```rust
use my_crate::pkix::AlgorithmIdentifier;
use my_crate::pkix::Name;
```

The prefix is prepended verbatim followed by `::`.

### Multiple import sources

ASN.1 may import from several modules in one `IMPORTS` block:

```asn1
IMPORTS
    Type1, Type2 FROM Module1
    Type3        FROM Module2
    Type4, Type5 FROM Module3;
```

Each source module produces one `use` statement. Multiple types from the same module
are grouped into a brace list:

```rust
use crate::module1::{Type1, Type2};
use crate::module2::Type3;
use crate::module3::{Type4, Type5};
```

---

## 8. no_std Support

The `--use-core` flag replaces `std::` paths with `core::` equivalents in the generated
code. This is required when targeting embedded or other no_std environments.

```sh
synta-codegen schema.asn1 --use-core -o src/generated.rs
```

Effect on generated code:

```rust
// Without --use-core
use std::convert::TryFrom;

// With --use-core
use core::convert::TryFrom;
```

The synta library itself supports no_std when its `std` feature is disabled. Generated
code with `--use-core` is compatible with that configuration.

---

## 9. build.rs Integration

synta-codegen can be called from a Cargo build script to regenerate types automatically
whenever the schema file changes.

Add synta-codegen as a build dependency in `Cargo.toml`:

```toml
[build-dependencies]
synta-codegen = { path = "../synta-codegen" }
```

In `build.rs`:

```rust
use std::path::PathBuf;
use synta_codegen::{parse, generate_with_config, CodeGenConfig};

fn main() {
    let schema = std::fs::read_to_string("schema.asn1")
        .expect("failed to read schema.asn1");

    let module = parse(&schema).expect("failed to parse ASN.1");

    // Use crate-relative imports; all other options default.
    let config = CodeGenConfig::with_crate_imports();

    let code = generate_with_config(&module, config)
        .expect("failed to generate Rust code");

    let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
    std::fs::write(out_dir.join("generated_types.rs"), code)
        .expect("failed to write generated_types.rs");

    println!("cargo:rerun-if-changed=schema.asn1");
}
```

In `src/lib.rs` (or wherever the types are needed):

```rust
include!(concat!(env!("OUT_DIR"), "/generated_types.rs"));
```

### Multi-module build.rs

When a project splits its schema across several ASN.1 files that import from each
other, list them in dependency order (base modules first) and generate each in turn:

```rust
use synta_codegen::{parse, generate_with_config, CodeGenConfig};
use std::{fs, path::PathBuf};

fn main() {
    // Modules listed in dependency order: base first, dependents after.
    let modules = ["base_types", "user_module", "admin_module"];

    let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
    let config = CodeGenConfig::with_crate_imports();

    for name in &modules {
        let schema = fs::read_to_string(format!("schemas/{name}.asn1"))
            .unwrap_or_else(|e| panic!("failed to read {name}.asn1: {e}"));
        let module = parse(&schema)
            .unwrap_or_else(|e| panic!("failed to parse {name}.asn1: {e}"));
        let code = generate_with_config(&module, config.clone())
            .unwrap_or_else(|e| panic!("failed to generate {name}: {e}"));
        fs::write(out_dir.join(format!("{name}.rs")), code)
            .unwrap_or_else(|e| panic!("failed to write {name}.rs: {e}"));
        println!("cargo:rerun-if-changed=schemas/{name}.asn1");
    }
}
```

In `src/lib.rs`:

```rust
mod base_types  { include!(concat!(env!("OUT_DIR"), "/base_types.rs"));  }
mod user_module { include!(concat!(env!("OUT_DIR"), "/user_module.rs")); }
mod admin_module{ include!(concat!(env!("OUT_DIR"), "/admin_module.rs"));}

pub use base_types::{BaseType1, BaseType2};
pub use user_module::User;
```

The generated `use crate::base_types::*` statements in `user_module.rs` resolve
correctly because the modules are declared at crate root.

### Recommended project layout

```
my-project/
+-- Cargo.toml
+-- build.rs
+-- schemas/
|   +-- base_types.asn1
|   +-- user_module.asn1
|   +-- admin_module.asn1
+-- src/
    +-- lib.rs        (contains the include! macros above)
    +-- manual.rs     (hand-written code that uses the generated types)
```

---

## 10. Library API Reference

### `parse`

```rust
pub fn parse(input: &str) -> Result<Module, ParseError>
```

Parses an ASN.1 module definition from `input` and returns a `Module` value
representing the parsed AST. Returns `ParseError` on syntax or semantic errors.

### `generate`

```rust
pub fn generate(module: &Module) -> Result<String, std::fmt::Error>
```

Generates Rust source code for `module` using default configuration: owned string
types, `std` paths, no import prefix. Returns the generated source as a `String`.

Use `generate_with_config` to customise any of these settings.

### `generate_with_config`

```rust
pub fn generate_with_config(
    module: &Module,
    config: CodeGenConfig,
) -> Result<String, std::fmt::Error>
```

Generates Rust source code applying the settings in `config`.

### `CodeGenConfig`

```rust
pub struct CodeGenConfig {
    /// Module path prefix for `use` statements emitted for ASN.1 IMPORTS.
    /// `None` (default): imports are annotated as comments only.
    pub module_path_prefix: Option<String>,

    /// Emit `core::convert::TryFrom` instead of `std::convert::TryFrom`.
    /// Required for `#![no_std]` targets. Default: `false`.
    pub use_core: bool,

    /// Type names from IMPORTS that should not be re-declared locally.
    pub skip_imported_types: std::collections::HashSet<String>,

    /// Lifetime requirements for imported types.
    /// Key: type name; value: lifetime string (e.g. `"'a"`).
    pub imported_type_lifetimes: std::collections::HashMap<String, String>,

    /// Whether string/binary types use owned or zero-copy borrowed forms.
    /// Default: `StringTypeMode::Owned`.
    pub string_type_mode: StringTypeMode,

    /// When `true`, `ANY` and `ANY DEFINED BY` fields are generated as
    /// `RawDer<'a>` instead of `Element<'a>` for zero-copy lazy decoding.
    /// Default: `false`.
    pub any_as_raw_der: bool,

    /// Controls how derive macros and their helper attributes are emitted.
    /// Default: `DeriveMode::FeatureGated`.
    pub derive_mode: DeriveMode,
}
```

Three convenience constructors set `module_path_prefix` and leave everything else at
its default:

```rust
// use crate::module_name::Type
let config = CodeGenConfig::with_crate_imports();

// use super::module_name::Type
let config = CodeGenConfig::with_super_imports();

// use my_lib::module_name::Type
let config = CodeGenConfig::with_custom_prefix("my_lib");
```

All three constructors produce configs with `StringTypeMode::Owned` and
`use_core: false`. Use struct-update syntax to override individual fields:

```rust
use synta_codegen::{CodeGenConfig, StringTypeMode};

let config = CodeGenConfig {
    string_type_mode: StringTypeMode::Borrowed,
    ..CodeGenConfig::with_crate_imports()
};
```

### `StringTypeMode`

```rust
pub enum StringTypeMode {
    Owned,    // default
    Borrowed,
}
```

Controls whether the five ASN.1 string/binary types that have a zero-copy `Ref`
variant are emitted as owned heap-allocating types or as borrowed references.
See [§11](#11-owned-vs-borrowed-string-types) for a full explanation.

### `DeriveMode`

```rust
pub enum DeriveMode {
    FeatureGated,      // default
    Always,
    Custom(String),
}
```

Controls how derive macros (`Asn1Sequence`, `Asn1Choice`, `Asn1Set`) and their
helper attributes are emitted in generated code.

- **`FeatureGated`** (default) — wraps every annotation in
  `#[cfg_attr(feature = "derive", …)]`. The consuming crate must expose a
  `derive` Cargo feature that pulls in `synta-derive`:

  ```toml
  [features]
  derive = ["dep:synta-derive"]
  ```

  This is the safest default: `synta-derive` is only compiled when the consumer
  opts in, keeping the mandatory dependency tree small.

- **`Always`** — emits `#[derive(Asn1Sequence)]` unconditionally with no
  `cfg_attr` gate. Use this when the consuming crate always depends on
  `synta-derive` and does not want to expose a separate `derive` feature.

  ```rust
  use synta_codegen::{parse, generate_with_config, CodeGenConfig, DeriveMode};

  let config = CodeGenConfig {
      derive_mode: DeriveMode::Always,
      ..CodeGenConfig::with_crate_imports()
  };
  let code = generate_with_config(&module, config)?;
  ```

- **`Custom(name)`** — uses a caller-supplied feature name in the `cfg_attr`
  guard instead of `"derive"`. Useful when the consuming crate exposes its own
  feature name for derive support:

  ```rust
  use synta_codegen::{parse, generate_with_config, CodeGenConfig, DeriveMode};

  let config = CodeGenConfig {
      derive_mode: DeriveMode::Custom("asn1-derive".to_string()),
      ..CodeGenConfig::with_crate_imports()
  };
  let code = generate_with_config(&module, config)?;
  ```

---

## 11. Owned vs. Borrowed String Types

Several ASN.1 string and binary types have two Rust representations in synta:

| ASN.1 type        | Owned form          | Zero-copy form          |
|-------------------|---------------------|-------------------------|
| `OCTET STRING`    | `OctetString`       | `OctetStringRef<'a>`    |
| `BIT STRING`      | `BitString`         | `BitStringRef<'a>`      |
| `UTF8String`      | `Utf8String`        | `Utf8StringRef<'a>`     |
| `PrintableString` | `PrintableString`   | `PrintableStringRef<'a>`|
| `IA5String`       | `IA5String`         | `IA5StringRef<'a>`      |

`CodeGenConfig::string_type_mode` selects which form synta-codegen emits.

### Default: Owned types

`StringTypeMode::Owned` (the default) emits the owned forms.  Owned types heap-allocate
their contents on decode and have no lifetime parameter.  This is the most convenient
choice when you need to construct structs programmatically (e.g. in tests or message
builders).

```asn1
Msg ::= SEQUENCE {
    label UTF8String,
    data  OCTET STRING
}
```

Generated Rust (default):

```rust
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "derive", derive(Asn1Sequence))]
pub struct Msg {
    pub label: Utf8String,
    pub data: OctetString,
}
```

### Borrowed mode: zero-copy Ref types

`StringTypeMode::Borrowed` emits the `Ref` forms that borrow directly from the decoder's
input buffer.  No heap allocation occurs for the string content at decode time.  This is
optimal for parse-only workloads such as X.509 certificate inspection, where structs are
decoded, inspected, and discarded — never constructed from scratch.

```rust
use synta_codegen::{CodeGenConfig, StringTypeMode};

let config = CodeGenConfig {
    string_type_mode: StringTypeMode::Borrowed,
    ..Default::default()
};
let code = generate_with_config(&module, config)?;
```

Generated Rust (borrowed mode):

```rust
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "derive", derive(Asn1Sequence))]
pub struct Msg<'a> {
    pub label: Utf8StringRef<'a>,
    pub data: OctetStringRef<'a>,
}
```

### Which types are affected

Only the five types listed in the table above are affected.  Other string types —
`TeletexString`, `UniversalString`, `BmpString`, `GeneralString`, `NumericString`,
`VisibleString` — have no `Ref` variant in synta and are always emitted as owned types
regardless of the mode.

Type aliases inherit the mode:

```asn1
MyLabel ::= UTF8String
```

Borrowed mode emits:

```rust
pub type MyLabel<'a> = Utf8StringRef<'a>;
```

### Lifetime propagation

When a struct contains a field of a borrowed type (directly or transitively), synta-codegen
adds a `'a` lifetime parameter to that struct.  Structs that only contain owned types or
unaffected types receive no lifetime parameter.

```asn1
Inner ::= SEQUENCE { name UTF8String }
Outer ::= SEQUENCE { inner Inner, count INTEGER }
```

Borrowed mode:

```rust
pub struct Inner<'a> { pub name: Utf8StringRef<'a> }
pub struct Outer<'a> { pub inner: Inner<'a>, pub count: Integer }
```

`Outer` gains `<'a>` because it contains `Inner<'a>`.

### Named bit strings are always owned

A `BIT STRING` with named bits (a named-bit list) is always emitted as an owned
`BitString` regardless of mode, because it is decoded into a concrete bit-field type
and the bit constants are expressed as plain `u32` offsets into it.

```asn1
KeyUsage ::= BIT STRING {
    digitalSignature (0),
    keyEncipherment  (2)
} (SIZE (32..MAX))
```

Generated Rust (both modes):

```rust
pub struct KeyUsage(pub synta::BitString);   // always owned

impl KeyUsage {
    pub const DIGITAL_SIGNATURE: u32 = 0;
    pub const KEY_ENCIPHERMENT: u32 = 2;
}
```

---

## What Is Not Generated

The following are parsed and understood by synta-codegen but produce no Rust output:

- Table constraints and information object class instances/object sets (X.681/X.682). CLASS definitions are parsed and emit a documentation comment but no DER-encodable type.
- Serde derive attributes (available as a separate synta-derive feature, not via codegen).
- Trait implementations beyond `Debug`, `Clone`, and `PartialEq`.
- Explicit encode/decode methods -- these are handled entirely by the synta library's
  derive macros at compile time.
- User-defined constraint validation hooks.