alef 0.84.1

Opinionated polyglot binding generator for Rust libraries
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
//! ABI/coordinate grammar validation for the C-ABI configuration surface.
//!
//! Every generator that reads `[crates.ffi]` splices some piece of that config
//! straight into a target file it does not fully control the syntax of: a Rust
//! string literal in generated `build.rs`, a Makefile variable assignment, a
//! `cbindgen.toml`/Cargo.toml table, or a POSIX shell script. Each of those hosts
//! has its own escaping rules and its own "active" constructs (Make's `$(...)`,
//! Bash's `$()`/backticks even inside double quotes, Rust's string-literal escapes,
//! Cargo's TOML table/key syntax). A single generic "identifier" or "safe string"
//! check cannot protect all of them without either missing a host's active
//! construct or rejecting a currently-valid configuration for a stricter host it
//! doesn't actually reach.
//!
//! This module has one function per target grammar. Each function's doc comment
//! names the grammar it enforces and cites where that grammar comes from, so a
//! future change can tell whether a given field's rule is still the right one
//! instead of re-deriving it from scratch. Callers are expected to wrap the
//! `Err(String)` returned here into a `ResolveError::InvalidConfig` (or an
//! `anyhow` context) that names the offending field and crate.

/// Validate `[ffi] header_name`.
///
/// **Grammar:** exactly one path component, ending in `.h`, drawn from the POSIX
/// portable filename character set (`A-Za-z0-9._-`) restricted further to exclude
/// `/` and `\` entirely so the value cannot smuggle in a second path component.
///
/// **Source:** the field's own doc comment ("used by cbindgen to declare the
/// return... `.h`") and the generic C `#include "header.h"` / cbindgen single-file
/// convention — there is no directory structure to preserve here, only a bare
/// filename. Restricting the charset to the POSIX portable set additionally rules
/// out the two hosts this value reaches raw: a Rust string literal in generated
/// `build.rs` (`Path::new("include/{header_name}")`) and Make `$(wildcard ...)`
/// patterns, both of which treat `"` / `$(` / newlines as active.
///
/// **Active construct beyond quote/backslash/control chars:** none in the
/// filename grammar itself — the risk here is path traversal (`/`, `\`, `..`
/// segments), not code execution, which is why this function forbids separators
/// outright rather than merely rejecting `..`.
pub fn validate_c_header_filename(value: &str) -> Result<(), String> {
    let Some(stem) = value.strip_suffix(".h") else {
        return Err(format!("`{value}` must end in `.h`"));
    };
    if stem.is_empty() {
        return Err("must have a non-empty name before `.h`".to_string());
    }
    if value.contains('/') || value.contains('\\') {
        return Err(format!(
            "`{value}` must be a single filename with no path separators (`/` or `\\`)"
        ));
    }
    let allowed = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.';
    if !value.chars().all(allowed) {
        return Err(format!(
            "`{value}` may only contain ASCII letters, digits, `_`, `-`, and `.`"
        ));
    }
    Ok(())
}

/// Validate a native artifact basename: `[ffi] lib_name`, the derived
/// release-artifact package name, and the e2e `c` package name.
///
/// **Grammar:** `^[A-Za-z0-9_][A-Za-z0-9_.+-]*$` — first character alphanumeric
/// or `_`, remaining characters alphanumeric, `_`, `-`, `.`, or `+`.
///
/// **Source:** pkg-config's own `.pc` basename convention (pkg-config(1) package
/// names commonly contain `+` and `.`, e.g. `gtk+-3.0`, `libxml-2.0`), which is
/// the strictest of the three hosts this value reaches raw (Rust
/// `cargo:rustc-link-arg` directives, a generated Makefile's `-l`/`$(shell
/// pkg-config ...)` lines, and a release tarball / URL path segment). Adopting
/// pkg-config's own charset is therefore also safe for the other two.
///
/// **Active construct beyond quote/backslash/control chars:** Make's `$(...)`
/// (a `$(shell ...)` call or a bare `$(name)` variable reference) and a raw
/// newline, which starts a new Makefile line/recipe. Excluding `$`, `(`, `)`,
/// and whitespace from the allowed charset blocks both.
pub fn validate_native_artifact_basename(value: &str) -> Result<(), String> {
    let mut chars = value.chars();
    let Some(first) = chars.next() else {
        return Err("must not be empty".to_string());
    };
    if !(first.is_ascii_alphanumeric() || first == '_') {
        return Err(format!("`{value}` must start with an ASCII letter, digit, or `_`"));
    }
    let rest_allowed = |c: char| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | '+');
    if !chars.all(rest_allowed) {
        return Err(format!(
            "`{value}` may only contain ASCII letters, digits, `_`, `-`, `.`, and `+` after the first character"
        ));
    }
    Ok(())
}

/// Validate a path embedded unquoted in the generated C Makefile.
pub fn validate_c_make_path(value: &str, output_base: &str) -> Result<(), String> {
    if value.is_empty() || std::path::Path::new(value).is_absolute() || value.contains('\\') {
        return Err(format!("`{value}` must be a non-empty relative POSIX path"));
    }
    if !value
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '/' | '.' | '_' | '-' | '+'))
    {
        return Err(format!("`{value}` contains a character active in Make or the shell"));
    }

    let mut depth = lexical_relative_depth(output_base)? + 1;
    for component in value.split('/') {
        match component {
            "" | "." => {}
            ".." if depth == 0 => return Err(format!("`{value}` escapes the repository root")),
            ".." => depth -= 1,
            _ => depth += 1,
        }
    }
    Ok(())
}

/// Validate that a generated C harness output root is a repository-relative path.
pub fn validate_c_output_base(value: &str) -> Result<(), String> {
    lexical_relative_depth(value).map(|_| ())
}

fn lexical_relative_depth(value: &str) -> Result<usize, String> {
    if value.starts_with('/') || value.contains('\\') || has_windows_drive_prefix(value) {
        return Err(format!(
            "output base `{value}` must be a repository-relative POSIX path"
        ));
    }
    let mut depth = 0;
    for component in value.split('/') {
        match component {
            "" | "." => {}
            ".." if depth == 0 => return Err(format!("output base `{value}` escapes the repository root")),
            ".." => depth -= 1,
            _ => depth += 1,
        }
    }
    Ok(depth)
}

fn has_windows_drive_prefix(value: &str) -> bool {
    let bytes = value.as_bytes();
    bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
}

pub fn relative_repo_path(from_directory: &str, target: &str) -> Result<String, String> {
    let from = normalized_relative_components(from_directory)?;
    let to = normalized_relative_components(target)?;
    let shared = from.iter().zip(&to).take_while(|(left, right)| left == right).count();
    let mut parts = vec![".."; from.len() - shared];
    parts.extend(to[shared..].iter().map(String::as_str));
    Ok(if parts.is_empty() {
        ".".to_string()
    } else {
        parts.join("/")
    })
}

fn normalized_relative_components(value: &str) -> Result<Vec<String>, String> {
    lexical_relative_depth(value)?;
    let mut parts = Vec::new();
    for component in value.split('/') {
        match component {
            "" | "." => {}
            ".." => {
                parts.pop();
            }
            normal => parts.push(normal.to_string()),
        }
    }
    Ok(parts)
}

/// Validate a standalone bare ASCII ABI identifier, such as a capsule's
/// `c_return_type`.
///
/// **Grammar:** `^[A-Za-z][A-Za-z0-9_]*$` — a portable file-scope C identifier
/// (ISO C §6.4.2.1) that excludes the implementation-reserved leading `_` space.
///
/// **Source:** this value becomes a complete cbindgen-declared C type name, so
/// it must also avoid C keywords. Every C-ABI backend this project generates
/// for only portably links ASCII C identifiers.
///
/// **Active construct beyond quote/backslash/control chars:** none for a bare
/// identifier destined for an identifier position — but `c_return_type` also
/// reaches a Rust string literal in generated `build.rs`
/// (`header.replace("{prefixed}", "{bare}")`), where a `"` would break out of the
/// literal. This grammar excludes `"` (and every other non-identifier character)
/// by construction, which is why the escaping step at that call site is
/// defense-in-depth rather than the only guard.
pub fn validate_ascii_abi_identifier(value: &str) -> Result<(), String> {
    validate_ascii_abi_prefix(value)?;
    if is_c_reserved_keyword(value) {
        return Err(format!("`{value}` is a reserved C keyword"));
    }
    Ok(())
}

/// Validate an ASCII ABI namespace prefix.
///
/// The prefix is always combined with another identifier component before it
/// becomes a C symbol, so a C keyword is valid here even though it is invalid
/// as a standalone identifier.
pub fn validate_ascii_abi_prefix(value: &str) -> Result<(), String> {
    let mut chars = value.chars();
    let Some(first) = chars.next() else {
        return Err("must not be empty".to_string());
    };
    if !first.is_ascii_alphabetic() {
        return Err(format!("`{value}` must start with an ASCII letter"));
    }
    if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
        return Err(format!(
            "`{value}` may only contain ASCII letters, digits, and `_` after the first character"
        ));
    }
    Ok(())
}

const C_RESERVED_KEYWORDS: &[&str] = &[
    "auto",
    "break",
    "case",
    "char",
    "const",
    "continue",
    "default",
    "do",
    "double",
    "else",
    "enum",
    "extern",
    "float",
    "for",
    "goto",
    "if",
    "inline",
    "int",
    "long",
    "register",
    "restrict",
    "return",
    "short",
    "signed",
    "sizeof",
    "static",
    "struct",
    "switch",
    "typedef",
    "union",
    "unsigned",
    "void",
    "volatile",
    "while",
    "_Alignas",
    "_Alignof",
    "_Atomic",
    "_Bool",
    "_Complex",
    "_Decimal32",
    "_Decimal64",
    "_Decimal128",
    "_Generic",
    "_Imaginary",
    "_Noreturn",
    "_Static_assert",
    "_Thread_local",
    "alignas",
    "alignof",
    "bool",
    "constexpr",
    "false",
    "nullptr",
    "static_assert",
    "thread_local",
    "true",
    "typeof",
    "typeof_unqual",
];

fn is_c_reserved_keyword(value: &str) -> bool {
    C_RESERVED_KEYWORDS.contains(&value)
}

/// Validate a capsule's `into_raw_type`: a fully-qualified Rust pointee type path
/// with no generic or const-generic arguments.
///
/// **Grammar:** `syn::TypePath`, no `qself` (no `<Foo as Trait>::Bar` syntax), and
/// every path segment's `PathArguments` must be `None` — i.e. `ident(::ident)*`
/// only.
///
/// **Source:** the field's own doc comment: "Fully-qualified Rust pointee type of
/// the `*const {into_raw_type}}` that `value.into_raw()` already returns" — every
/// real example (`tree_sitter::ffi::TSLanguage`) is a bare module path with no
/// generics, so forbidding generics entirely accepts every currently-documented
/// use while closing the injection surface a generic-argument grammar would
/// otherwise admit.
///
/// **Active construct beyond quote/backslash/control chars:** a generic
/// argument list may contain a const-generic block, `Foo<{ EXPR }>`, which is an
/// arbitrary const-evaluated Rust expression — a real code-execution position
/// inside what looks like "just a type." Requiring every segment's
/// `PathArguments` to be `None` rejects any `<...>` outright, so this construct
/// can never appear.
pub fn validate_rust_pointee_type_path(value: &str) -> Result<(), String> {
    let parsed: syn::TypePath =
        syn::parse_str(value).map_err(|error| format!("`{value}` is not a Rust type path: {error}"))?;
    if parsed.qself.is_some() {
        return Err(format!(
            "`{value}` must not use qualified-path syntax (`<Type as Trait>::...`)"
        ));
    }
    for segment in &parsed.path.segments {
        if !matches!(segment.arguments, syn::PathArguments::None) {
            return Err(format!(
                "`{value}` must not carry generic arguments (segment `{}` has some)",
                segment.ident
            ));
        }
    }
    Ok(())
}

/// Validate a `[[crates.ffi.target_dep_overrides]] cfg` expression.
///
/// **Grammar:** a restricted subset of `syn::Expr` mirroring `rustc`'s actual
/// `cfg(...)` attribute grammar (see the Rust reference, "Conditional
/// compilation"): a bare identifier (`unix`), a `key = "string"` predicate whose
/// value is a double-quoted string literal, or an `any(...)` / `all(...)` /
/// `not(...)` call whose arguments are themselves recursively valid under this
/// same grammar.
///
/// **Source:** this is the exact grammar `#[cfg(...)]` accepts; parsing through
/// `syn` (rather than string-matching for `any(`/`all(`/`not(`) means a malformed
/// or hostile value fails to parse instead of silently matching a substring.
///
/// **Active construct beyond quote/backslash/control chars:** a char literal
/// (`'x'`, `syn::Lit::Char`) is a valid `syn::Expr` and a valid right-hand side
/// of a `key = 'x'` assignment — every bit as syntactically legal as `key =
/// "x"` — yet it contains a literal `'` that is completely inert in Rust. This
/// value is spliced into a TOML *literal* string, `[target.'cfg({cfg})'...]`,
/// where TOML forbids `'` inside literal strings entirely, so a raw `'` here
/// breaks out of that table header. Labeled loops/blocks (`'a: loop { ... }`)
/// carry the same raw `'` and are likewise valid, unrelated `syn::Expr`
/// variants. Restricting assignment values to `Lit::Str` and restricting every
/// expression shape to bare-path / assign / any-all-not-call (the wildcard arm
/// rejects everything else, including loops) closes both off; nothing accepted
/// by this grammar can produce a literal `'` in the source text.
pub fn validate_cfg_expression(value: &str) -> Result<(), String> {
    if value.chars().any(|ch| ch == '\'' || ch.is_control()) {
        return Err("cfg expression contains a character that is active in its TOML literal-string host".to_string());
    }
    let expr: syn::Expr =
        syn::parse_str(value).map_err(|error| format!("`{value}` is not a valid cfg expression: {error}"))?;
    validate_cfg_expr_shape(&expr)
}

fn validate_cfg_expr_shape(expr: &syn::Expr) -> Result<(), String> {
    match expr {
        syn::Expr::Path(p) if p.qself.is_none() => validate_cfg_bare_path(&p.path),
        syn::Expr::Assign(assign) => {
            let syn::Expr::Path(lhs) = assign.left.as_ref() else {
                return Err("cfg predicate key must be a bare identifier".to_string());
            };
            validate_cfg_bare_path(&lhs.path)?;
            match assign.right.as_ref() {
                syn::Expr::Lit(syn::ExprLit {
                    lit: syn::Lit::Str(_), ..
                }) => Ok(()),
                _ => Err("cfg predicate value must be a double-quoted string literal".to_string()),
            }
        }
        syn::Expr::Call(call) => {
            let syn::Expr::Path(func) = call.func.as_ref() else {
                return Err("cfg combinator must be a bare `any`/`all`/`not`".to_string());
            };
            let Some(name) = func.path.get_ident().map(ToString::to_string) else {
                return Err("cfg combinator must be a single bare identifier".to_string());
            };
            if !matches!(name.as_str(), "any" | "all" | "not") {
                return Err(format!("`{name}` is not a valid cfg combinator (expected any/all/not)"));
            }
            if name == "not" && call.args.len() != 1 {
                return Err("cfg combinator `not` requires exactly one argument".to_string());
            }
            for arg in &call.args {
                validate_cfg_expr_shape(arg)?;
            }
            Ok(())
        }
        _ => Err(
            "unsupported cfg expression shape — expected a bare key, `key = \"value\"`, or any/all/not(...)"
                .to_string(),
        ),
    }
}

fn validate_cfg_bare_path(path: &syn::Path) -> Result<(), String> {
    let Some(ident) = path.get_ident() else {
        return Err("cfg key must be a single bare identifier, not a `::`-separated path".to_string());
    };
    let text = ident.to_string();
    if text.starts_with("r#") {
        return Err(format!("`{text}` must not be a raw identifier"));
    }
    validate_cfg_identifier(&text)
}

fn validate_cfg_identifier(value: &str) -> Result<(), String> {
    let mut chars = value.chars();
    let Some(first) = chars.next() else {
        return Err("cfg key must not be empty".to_string());
    };
    if !(first.is_ascii_alphabetic() || first == '_')
        || !chars.all(|character| character.is_ascii_alphanumeric() || character == '_')
    {
        return Err(format!("`{value}` is not a portable cfg identifier"));
    }
    Ok(())
}

/// Validate a capsule's `package` field: a Cargo package name injected as a bare
/// TOML table key (`{package} = "{version}"`).
///
/// **Grammar:** first character an ASCII letter, remaining characters ASCII
/// alphanumeric, `_`, or `-` — the practical shape of every published crates.io
/// package name.
///
/// **Source:** Cargo's package-name rules (the Cargo Book, "The Manifest
/// Format" § `package.name`).
///
/// **Active construct beyond quote/backslash/control chars:** a bare (unquoted)
/// TOML key ends at the first `=`, `.`, `[`, `]`, or newline — any of those
/// characters lets the value terminate the key early and start a new TOML
/// table/key, e.g. injecting a `[patch.crates-io]` table. This grammar excludes
/// all of them.
pub fn validate_cargo_package_name(value: &str) -> Result<(), String> {
    let mut chars = value.chars();
    let Some(first) = chars.next() else {
        return Err("must not be empty".to_string());
    };
    if !first.is_ascii_alphabetic() {
        return Err(format!("`{value}` must start with an ASCII letter"));
    }
    if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') {
        return Err(format!(
            "`{value}` may only contain ASCII letters, digits, `_`, and `-` after the first character"
        ));
    }
    Ok(())
}

/// Validate a capsule's `package_version` field: a Cargo dependency version
/// requirement injected inside a quoted TOML string (`{package} = "{version}"`).
///
/// **Grammar:** `semver::VersionReq` — Cargo's actual dependency version
/// requirement syntax (`"1"`, `"^1.2"`, `">=1, <2"`, ...), not a bare
/// `semver::Version`, since this is the right-hand side of a `[dependencies]`
/// entry rather than a package's own declared version.
///
/// **Source:** the `semver` crate's `VersionReq`, which is what Cargo itself
/// uses to parse `[dependencies]` version strings.
///
/// **Active construct beyond quote/backslash/control chars:** a raw `"` closes
/// the TOML string early. `VersionReq`'s grammar has no representation for `"`
/// (or any other TOML-active character), so a successful parse already excludes
/// it.
pub fn validate_cargo_version_req(value: &str) -> Result<(), String> {
    semver::VersionReq::parse(value)
        .map(|_| ())
        .map_err(|error| format!("`{value}` is not a valid Cargo version requirement: {error}"))
}

/// Validate a Cargo feature name: `[ffi] features`, `extra_features`,
/// `excluded_default_features`, and `target_dep_overrides[].features` entries.
///
/// **Grammar:** first character ASCII alphanumeric or `_`, remaining characters
/// ASCII alphanumeric, `_`, `-`, or `.` — Cargo's plain feature-name shape.
/// Deliberately excludes `/` and `:`, which are meaningful in a full Cargo
/// *feature specification* (`pkg/feat`, `dep:pkg`) — every one of these fields is
/// spliced as a **plain feature name** on both sides (the FFI crate's own new
/// feature and the forwarded `"{core-crate}/{name}"` dependency feature), so a
/// value containing `/` would forge a second, attacker-chosen dependency feature
/// reference rather than naming a feature.
///
/// **Source:** the Cargo Book, "Features" § feature names.
///
/// **Active construct beyond quote/backslash/control chars:** `"` closes the
/// surrounding TOML string (`features = ["{name}"]` / `{name} = [...]` key)
/// early; `/` reinterprets the name as a `pkg/feature` reference into a
/// different, attacker-named dependency. Both are excluded by the charset.
pub fn validate_cargo_feature_name(value: &str) -> Result<(), String> {
    let mut chars = value.chars();
    let Some(first) = chars.next() else {
        return Err("must not be empty".to_string());
    };
    if !(first.is_ascii_alphanumeric() || first == '_') {
        return Err(format!("`{value}` must start with an ASCII letter, digit, or `_`"));
    }
    let rest_allowed = |c: char| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.');
    if !chars.all(rest_allowed) {
        return Err(format!(
            "`{value}` may only contain ASCII letters, digits, `_`, `-`, and `.` after the first character"
        ));
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    // -- header filename ----------------------------------------------------

    #[test]
    fn header_filename_accepts_default_shape() {
        assert_eq!(validate_c_header_filename("my_lib.h"), Ok(()));
        assert_eq!(validate_c_header_filename("custom.h"), Ok(()));
    }

    #[test]
    fn header_filename_rejects_path_traversal() {
        assert!(validate_c_header_filename("../../etc/passwd.h").is_err());
        assert!(validate_c_header_filename("sub/dir.h").is_err());
        assert!(validate_c_header_filename("dir\\file.h").is_err());
    }

    #[test]
    fn header_filename_rejects_rust_string_breakout() {
        assert!(validate_c_header_filename("evil\".h").is_err());
        assert!(validate_c_header_filename("evil\nfn x(){}.h").is_err());
    }

    #[test]
    fn header_filename_requires_h_suffix() {
        assert!(validate_c_header_filename("my_lib").is_err());
        assert!(validate_c_header_filename(".h").is_err());
    }

    // -- native artifact basename --------------------------------------------

    #[test]
    fn artifact_basename_accepts_default_shape() {
        assert_eq!(validate_native_artifact_basename("my_lib_ffi"), Ok(()));
        assert_eq!(validate_native_artifact_basename("libmy_custom"), Ok(()));
        assert_eq!(validate_native_artifact_basename("gtk+-3.0"), Ok(()));
    }

    #[test]
    fn artifact_basename_rejects_make_shell_canary() {
        assert!(validate_native_artifact_basename("$(shell rm -rf /)").is_err());
        assert!(validate_native_artifact_basename("foo)\nevil:\n\trm -rf /").is_err());
    }

    #[test]
    fn artifact_basename_rejects_whitespace_and_slash() {
        assert!(validate_native_artifact_basename("my lib").is_err());
        assert!(validate_native_artifact_basename("my/lib").is_err());
    }

    #[test]
    fn c_make_path_rejects_expansion_and_repository_escape() {
        assert_eq!(validate_c_make_path("../../crates/sample-ffi", "e2e"), Ok(()));
        assert!(validate_c_make_path("../../$(shell touch pwned)", "e2e").is_err());
        assert!(validate_c_make_path("../../../outside", "e2e").is_err());
        assert!(validate_c_make_path("../../../outside", "e2e/nested/../..").is_err());
        assert!(validate_c_make_path("../crates/sample-ffi", "/tmp/e2e").is_err());
        assert!(validate_c_make_path("../crates/sample-ffi", r"C:\tmp\e2e").is_err());
        assert!(validate_c_make_path("../crates/sample-ffi", "../outside/e2e").is_err());
    }

    #[test]
    fn c_output_base_rejects_windows_drive_absolute_path() {
        assert!(validate_c_output_base("C:/tmp/e2e").is_err());
        assert!(validate_c_output_base("C:tmp/e2e").is_err());
    }

    // -- ascii abi identifier -------------------------------------------------

    #[test]
    fn abi_identifier_accepts_default_shape() {
        assert_eq!(validate_ascii_abi_identifier("my_lib"), Ok(()));
        assert_eq!(validate_ascii_abi_identifier("TSLanguage"), Ok(()));
    }

    #[test]
    fn abi_prefix_accepts_keyword_when_used_as_namespace() {
        assert_eq!(validate_ascii_abi_prefix("int"), Ok(()));
        assert!(validate_ascii_abi_identifier("int").is_err());
    }

    #[test]
    fn abi_identifier_rejects_quote_breakout() {
        assert!(validate_ascii_abi_identifier("evil\", \"x").is_err());
        assert!(validate_ascii_abi_identifier("evil-prefix").is_err());
        assert!(validate_ascii_abi_identifier("123start").is_err());
        assert!(validate_ascii_abi_identifier("int").is_err());
        assert!(validate_ascii_abi_identifier("__private").is_err());
        assert!(validate_ascii_abi_identifier("_Private").is_err());
        assert!(validate_ascii_abi_identifier("_private").is_err());
    }

    // -- rust pointee type path -----------------------------------------------

    #[test]
    fn pointee_type_path_accepts_documented_example() {
        assert_eq!(validate_rust_pointee_type_path("tree_sitter::ffi::TSLanguage"), Ok(()));
        assert_eq!(validate_rust_pointee_type_path("MyRawType"), Ok(()));
    }

    #[test]
    fn pointee_type_path_rejects_const_generic_block() {
        assert!(validate_rust_pointee_type_path("Foo<{ std::process::exit(1) }>").is_err());
    }

    #[test]
    fn pointee_type_path_rejects_qualified_path() {
        assert!(validate_rust_pointee_type_path("<Foo as Trait>::Bar").is_err());
    }

    #[test]
    fn pointee_type_path_rejects_garbage() {
        assert!(validate_rust_pointee_type_path("not a type; std::process::exit(1)").is_err());
    }

    // -- cfg expression --------------------------------------------------------

    #[test]
    fn cfg_expression_accepts_documented_example() {
        assert_eq!(
            validate_cfg_expression("all(target_os = \"android\", target_arch = \"x86_64\")"),
            Ok(())
        );
        assert_eq!(validate_cfg_expression("unix"), Ok(()));
        assert_eq!(validate_cfg_expression("not(windows)"), Ok(()));
        assert_eq!(validate_cfg_expression("_custom"), Ok(()));
    }

    #[test]
    fn cfg_expression_rejects_char_literal_quote_breakout() {
        // A char literal is valid syn::Expr syntax but contains a raw `'` that
        // would break out of the TOML `'cfg(...)'` literal-string table header.
        assert!(validate_cfg_expression("target_os = 'x'").is_err());
    }

    #[test]
    fn cfg_expression_rejects_labeled_loop_quote_breakout() {
        // A labeled loop is a valid, unrelated `syn::Expr` variant that still
        // carries a raw `'` in its source text — the wildcard arm must catch it.
        assert!(validate_cfg_expression("'a: loop { break }").is_err());
    }

    #[test]
    fn cfg_expression_rejects_non_string_and_unknown_combinator() {
        assert!(validate_cfg_expression("target_os = 1").is_err());
        assert!(validate_cfg_expression("evil(target_os = \"x\")").is_err());
        assert!(validate_cfg_expression("target_os = \"foo'bar\"").is_err());
        assert!(validate_cfg_expression("not()").is_err());
        assert!(validate_cfg_expression("not(unix, windows)").is_err());
    }

    // -- cargo package name ------------------------------------------------------

    #[test]
    fn cargo_package_name_accepts_typical_names() {
        assert_eq!(validate_cargo_package_name("tree-sitter"), Ok(()));
        assert_eq!(validate_cargo_package_name("serde_json"), Ok(()));
    }

    #[test]
    fn cargo_package_name_rejects_toml_table_injection() {
        assert!(validate_cargo_package_name("x\"\n[patch.crates-io]\nfoo").is_err());
        assert!(validate_cargo_package_name("x = \"1\"\n[dependencies]\ny").is_err());
        assert!(validate_cargo_package_name("1starts-with-digit").is_err());
    }

    // -- cargo version requirement -----------------------------------------------

    #[test]
    fn cargo_version_req_accepts_typical_requirements() {
        assert_eq!(validate_cargo_version_req("1.0"), Ok(()));
        assert_eq!(validate_cargo_version_req("^1.2.3"), Ok(()));
    }

    #[test]
    fn cargo_version_req_rejects_quote_breakout() {
        assert!(validate_cargo_version_req("1.0\"\n[patch.crates-io]").is_err());
    }

    // -- cargo feature name -------------------------------------------------------

    #[test]
    fn cargo_feature_name_accepts_typical_names() {
        assert_eq!(validate_cargo_feature_name("native-http"), Ok(()));
        assert_eq!(validate_cargo_feature_name("android-target"), Ok(()));
    }

    #[test]
    fn cargo_feature_name_rejects_dependency_feature_spec() {
        assert!(validate_cargo_feature_name("other-pkg/evil-feature").is_err());
        assert!(validate_cargo_feature_name("dep:evil").is_err());
        assert!(validate_cargo_feature_name("evil\"]\nfoo").is_err());
    }
}