1use std::{
2 collections::{BTreeSet, HashMap, HashSet},
3 fs::{self, File},
4 io::Read,
5 path::{Component, Path, PathBuf},
6};
7
8use anyhow::{Context, Result, bail};
9use serde::{Deserialize, Serialize};
10
11const GENERATED_NOTICE: &str = "Auto-generated. Do not edit directly.";
12pub const BRIDGE_SHIM_HEADER_FILE: &str = "include/VesperPlayerKitBridgeShim.h";
13pub const BRIDGE_SHIM_SOURCE_FILE: &str = "VesperPlayerKitBridgeShim.c";
14const MAX_BRIDGE_MANIFEST_BYTES: usize = 4 * 1024 * 1024;
15const MAX_BRIDGE_FRAGMENT_BYTES: usize = 1024 * 1024;
16const MAX_BRIDGE_SOURCE_ITEMS: usize = 512;
17const MAX_BRIDGE_DECLARATIONS: usize = 1024;
18const MAX_BRIDGE_INCLUDES: usize = 256;
19const MAX_BRIDGE_FFI_SYMBOLS_PER_WRAPPER: usize = 64;
20const MAX_GENERATED_BRIDGE_HEADER_BYTES: usize = 4 * 1024 * 1024;
21const MAX_GENERATED_BRIDGE_SOURCE_BYTES: usize = 16 * 1024 * 1024;
22
23#[derive(Debug, Deserialize, Serialize)]
24struct Manifest {
25 header_guard: String,
26 header_includes: Vec<String>,
27 c_includes: Vec<String>,
28 public_declarations: Vec<Declaration>,
29 private_declarations: Vec<Declaration>,
30 source_items: Vec<SourceItem>,
31}
32
33#[derive(Debug, Clone, Deserialize, Serialize)]
34#[serde(tag = "kind")]
35enum Declaration {
36 #[serde(rename = "enum")]
37 Enum {
38 name: String,
39 variants: Vec<EnumVariant>,
40 },
41 #[serde(rename = "struct")]
42 Struct { name: String, fields: Vec<Field> },
43 #[serde(rename = "function")]
44 Function {
45 return_type: String,
46 name: String,
47 parameters: Vec<Parameter>,
48 #[serde(default)]
49 storage: FunctionStorage,
50 },
51}
52
53#[derive(Debug, Clone, Default, Deserialize, Serialize)]
54#[serde(rename_all = "snake_case")]
55enum FunctionStorage {
56 #[default]
57 Public,
58 Extern,
59}
60
61#[derive(Debug, Clone, Deserialize, Serialize)]
62struct EnumVariant {
63 name: String,
64 value: i64,
65}
66
67#[derive(Debug, Clone, Deserialize, Serialize)]
68struct Field {
69 ty: String,
70 name: String,
71}
72
73#[derive(Debug, Clone, Deserialize, Serialize)]
74struct Parameter {
75 ty: String,
76 name: String,
77}
78
79#[derive(Debug, Deserialize, Serialize)]
80#[serde(tag = "kind")]
81enum SourceItem {
82 #[serde(rename = "static_fragment")]
83 StaticFragment { path: String },
84 #[serde(rename = "wrapper")]
85 Wrapper {
86 function: String,
87 ffi_symbols: Vec<String>,
88 ownership: Vec<String>,
89 body_fragment: String,
90 },
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct GeneratedShim {
95 header: String,
96 source: String,
97 required_ffi_symbols: Vec<String>,
98}
99
100impl GeneratedShim {
101 pub fn header(&self) -> &str {
102 &self.header
103 }
104
105 pub fn source(&self) -> &str {
106 &self.source
107 }
108
109 pub fn required_ffi_symbols(&self) -> &[String] {
110 &self.required_ffi_symbols
111 }
112}
113
114struct Cli {
115 command: Command,
116 manifest_path: PathBuf,
117 output_dir: PathBuf,
118 source_header_path: Option<PathBuf>,
119 source_c_path: Option<PathBuf>,
120}
121
122enum Command {
123 Generate,
124 Bootstrap,
125}
126
127pub fn run_cli(args: impl IntoIterator<Item = String>) -> Result<()> {
128 let cli = Cli::parse(args)?;
129 match cli.command {
130 Command::Generate => {
131 let generated = generate_from_manifest(&cli.manifest_path)?;
132 write_generated_directory(&cli.output_dir, &generated)?;
133 }
134 Command::Bootstrap => {
135 let header_path = cli
136 .source_header_path
137 .as_deref()
138 .context("bootstrap requires --source-header")?;
139 let c_path = cli
140 .source_c_path
141 .as_deref()
142 .context("bootstrap requires --source-c")?;
143 bootstrap_from_sources(&cli.manifest_path, &cli.output_dir, header_path, c_path)?;
144 }
145 }
146 Ok(())
147}
148
149impl Cli {
150 fn parse(args: impl IntoIterator<Item = String>) -> Result<Self> {
151 let mut command = None;
152 let mut manifest_path = None;
153 let mut output_dir = None;
154 let mut source_header_path = None;
155 let mut source_c_path = None;
156
157 let mut args = args.into_iter();
158 while let Some(arg) = args.next() {
159 match arg.as_str() {
160 "generate" => command = Some(Command::Generate),
161 "bootstrap" => command = Some(Command::Bootstrap),
162 "--manifest" => manifest_path = Some(next_path(&mut args, "--manifest")?),
163 "--out-dir" => output_dir = Some(next_path(&mut args, "--out-dir")?),
164 "--source-header" => {
165 source_header_path = Some(next_path(&mut args, "--source-header")?)
166 }
167 "--source-c" => source_c_path = Some(next_path(&mut args, "--source-c")?),
168 "-h" | "--help" => {
169 print_usage();
170 std::process::exit(0);
171 }
172 _ => bail!("unknown argument: {arg}"),
173 }
174 }
175
176 Ok(Self {
177 command: command.context("missing command: generate or bootstrap")?,
178 manifest_path: manifest_path.context("missing --manifest")?,
179 output_dir: output_dir.context("missing --out-dir")?,
180 source_header_path,
181 source_c_path,
182 })
183 }
184}
185
186fn next_path(args: &mut impl Iterator<Item = String>, flag: &str) -> Result<PathBuf> {
187 Ok(PathBuf::from(
188 args.next()
189 .with_context(|| format!("{flag} requires a value"))?,
190 ))
191}
192
193fn print_usage() {
194 eprintln!(
195 "Usage:\n player-ios-bridge-shim-generator generate --manifest <path> --out-dir <dir>\n player-ios-bridge-shim-generator bootstrap --manifest <path> --out-dir <fragment-dir> --source-header <path> --source-c <path>"
196 );
197}
198
199fn manifest_base_dir(manifest_path: &Path) -> PathBuf {
200 manifest_path
201 .parent()
202 .map_or_else(|| PathBuf::from("."), Path::to_path_buf)
203}
204
205fn validate_manifest(manifest: &Manifest) -> Result<()> {
206 if manifest.header_includes.len() > MAX_BRIDGE_INCLUDES
207 || manifest.c_includes.len() > MAX_BRIDGE_INCLUDES
208 {
209 bail!("bridge manifest exceeds {MAX_BRIDGE_INCLUDES} includes per generated file");
210 }
211 if manifest.public_declarations.len() > MAX_BRIDGE_DECLARATIONS
212 || manifest.private_declarations.len() > MAX_BRIDGE_DECLARATIONS
213 {
214 bail!("bridge manifest exceeds {MAX_BRIDGE_DECLARATIONS} declarations per section");
215 }
216 if manifest.source_items.len() > MAX_BRIDGE_SOURCE_ITEMS {
217 bail!("bridge manifest exceeds {MAX_BRIDGE_SOURCE_ITEMS} source items");
218 }
219
220 let mut public_functions = HashSet::new();
221 for declaration in &manifest.public_declarations {
222 if let Declaration::Function { name, .. } = declaration
223 && !public_functions.insert(name.as_str())
224 {
225 bail!("bridge manifest contains duplicate public function: {name}");
226 }
227 }
228 let mut wrapper_functions = HashSet::new();
229 for item in &manifest.source_items {
230 if let SourceItem::Wrapper {
231 function,
232 ffi_symbols,
233 ..
234 } = item
235 {
236 if ffi_symbols.len() > MAX_BRIDGE_FFI_SYMBOLS_PER_WRAPPER {
237 bail!(
238 "bridge wrapper {function} exceeds {MAX_BRIDGE_FFI_SYMBOLS_PER_WRAPPER} FFI symbols"
239 );
240 }
241 if !wrapper_functions.insert(function.as_str()) {
242 bail!("bridge manifest contains duplicate wrapper: {function}");
243 }
244 }
245 }
246 if let Some(function) = public_functions.difference(&wrapper_functions).next() {
247 bail!("bridge public function has no wrapper source item: {function}");
248 }
249 Ok(())
250}
251
252fn read_bounded_utf8_file(path: &Path, maximum_bytes: usize, label: &str) -> Result<String> {
253 let path_metadata = fs::symlink_metadata(path)
254 .with_context(|| format!("failed to inspect {label}: {}", path.display()))?;
255 if !path_metadata.file_type().is_file() {
256 bail!(
257 "{label} is not a regular non-symlink file: {}",
258 path.display()
259 );
260 }
261 let mut file =
262 File::open(path).with_context(|| format!("failed to open {label}: {}", path.display()))?;
263 let metadata = file
264 .metadata()
265 .with_context(|| format!("failed to inspect opened {label}: {}", path.display()))?;
266 if !metadata.is_file() {
267 bail!("{label} is not a regular file: {}", path.display());
268 }
269 if metadata.len() > maximum_bytes as u64 {
270 bail!("{label} exceeds {maximum_bytes} bytes: {}", path.display());
271 }
272 let mut bytes = Vec::with_capacity(metadata.len() as usize);
273 file.by_ref()
274 .take((maximum_bytes + 1) as u64)
275 .read_to_end(&mut bytes)
276 .with_context(|| format!("failed to read {label}: {}", path.display()))?;
277 if bytes.len() > maximum_bytes {
278 bail!("{label} exceeds {maximum_bytes} bytes: {}", path.display());
279 }
280 String::from_utf8(bytes).with_context(|| format!("{label} is not UTF-8: {}", path.display()))
281}
282
283fn read_bridge_fragment(manifest_dir: &Path, configured_path: &str, label: &str) -> Result<String> {
284 let mut relative = PathBuf::new();
285 for component in Path::new(configured_path).components() {
286 match component {
287 Component::Normal(value) => relative.push(value),
288 Component::CurDir => {}
289 Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
290 bail!(
291 "{label} must use a relative fragment path without parent traversal: {configured_path}"
292 );
293 }
294 }
295 }
296 if relative.as_os_str().is_empty() {
297 bail!("{label} must use a non-empty relative fragment path");
298 }
299
300 let base = fs::canonicalize(manifest_dir).with_context(|| {
301 format!(
302 "failed to resolve bridge manifest directory: {}",
303 manifest_dir.display()
304 )
305 })?;
306 let base_metadata = fs::symlink_metadata(&base).with_context(|| {
307 format!(
308 "failed to inspect bridge manifest directory: {}",
309 base.display()
310 )
311 })?;
312 if !base_metadata.file_type().is_dir() {
313 bail!(
314 "bridge manifest directory is not a regular non-symlink directory: {}",
315 base.display()
316 );
317 }
318
319 let component_count = relative.components().count();
320 let mut current = base;
321 for (index, component) in relative.components().enumerate() {
322 let Component::Normal(value) = component else {
323 continue;
324 };
325 current.push(value);
326 let metadata = fs::symlink_metadata(¤t)
327 .with_context(|| format!("failed to inspect {label}: {}", current.display()))?;
328 if index + 1 == component_count {
329 if !metadata.file_type().is_file() {
330 bail!(
331 "{label} is not a regular non-symlink file: {}",
332 current.display()
333 );
334 }
335 } else if !metadata.file_type().is_dir() {
336 bail!(
337 "{label} parent is not a regular non-symlink directory: {}",
338 current.display()
339 );
340 }
341 }
342 read_bounded_utf8_file(¤t, MAX_BRIDGE_FRAGMENT_BYTES, label)
343}
344
345fn read_manifest(path: &Path) -> Result<Manifest> {
346 let content = read_bounded_utf8_file(path, MAX_BRIDGE_MANIFEST_BYTES, "bridge manifest")?;
347 let manifest = serde_json::from_str(&content)
348 .with_context(|| format!("failed to parse manifest: {}", path.display()))?;
349 validate_manifest(&manifest)?;
350 Ok(manifest)
351}
352
353pub fn write_generated_directory(output_dir: &Path, generated: &GeneratedShim) -> Result<()> {
354 let include_dir = output_dir.join("include");
355 fs::create_dir_all(&include_dir).with_context(|| {
356 format!(
357 "failed to create output include dir: {}",
358 include_dir.display()
359 )
360 })?;
361 fs::write(output_dir.join(BRIDGE_SHIM_HEADER_FILE), &generated.header)
362 .with_context(|| "failed to write generated bridge shim header")?;
363 fs::write(output_dir.join(BRIDGE_SHIM_SOURCE_FILE), &generated.source)
364 .with_context(|| "failed to write generated bridge shim source")?;
365 Ok(())
366}
367
368pub fn generate_from_manifest(manifest_path: &Path) -> Result<GeneratedShim> {
369 let manifest = read_manifest(manifest_path)?;
370 generate(&manifest, manifest_base_dir(manifest_path).as_path())
371}
372
373fn generate(manifest: &Manifest, manifest_dir: &Path) -> Result<GeneratedShim> {
374 let header = generate_header(manifest)?;
375 let source = generate_source(manifest, manifest_dir)?;
376 let declared_ffi_symbols = manifest
377 .source_items
378 .iter()
379 .filter_map(|item| match item {
380 SourceItem::Wrapper { ffi_symbols, .. } => Some(ffi_symbols),
381 SourceItem::StaticFragment { .. } => None,
382 })
383 .flatten()
384 .cloned()
385 .collect::<BTreeSet<_>>();
386 let referenced_ffi_symbols = sorted_player_ffi_symbols(&source)
387 .into_iter()
388 .collect::<BTreeSet<_>>();
389 let missing_from_manifest = referenced_ffi_symbols
390 .difference(&declared_ffi_symbols)
391 .cloned()
392 .collect::<Vec<_>>();
393 let missing_from_generated_source = declared_ffi_symbols
394 .difference(&referenced_ffi_symbols)
395 .cloned()
396 .collect::<Vec<_>>();
397 if !missing_from_manifest.is_empty() || !missing_from_generated_source.is_empty() {
398 let mut details = Vec::new();
399 if !missing_from_manifest.is_empty() {
400 details.push(format!(
401 "missing from manifest ffi_symbols: {}",
402 missing_from_manifest.join(", ")
403 ));
404 }
405 if !missing_from_generated_source.is_empty() {
406 details.push(format!(
407 "declared but absent from generated C: {}",
408 missing_from_generated_source.join(", ")
409 ));
410 }
411 bail!(
412 "bridge manifest ffi_symbols do not match generated C references; {}",
413 details.join("; ")
414 );
415 }
416 let required_ffi_symbols = referenced_ffi_symbols.into_iter().collect();
417 Ok(GeneratedShim {
418 header,
419 source,
420 required_ffi_symbols,
421 })
422}
423
424fn generate_header(manifest: &Manifest) -> Result<String> {
425 let mut output = String::new();
426 output.push_str("/* ");
427 output.push_str(GENERATED_NOTICE);
428 output.push_str(" */\n");
429 output.push_str("#ifndef ");
430 output.push_str(&manifest.header_guard);
431 output.push('\n');
432 output.push_str("#define ");
433 output.push_str(&manifest.header_guard);
434 output.push_str("\n\n");
435 push_includes(&mut output, &manifest.header_includes);
436 output.push('\n');
437 push_declarations(&mut output, &manifest.public_declarations)?;
438 output.push_str("#endif\n");
439 if output.len() > MAX_GENERATED_BRIDGE_HEADER_BYTES {
440 bail!("generated bridge header exceeds {MAX_GENERATED_BRIDGE_HEADER_BYTES} bytes");
441 }
442 Ok(output)
443}
444
445fn generate_source(manifest: &Manifest, manifest_dir: &Path) -> Result<String> {
446 let mut output = String::new();
447 output.push_str("/* ");
448 output.push_str(GENERATED_NOTICE);
449 output.push_str(" */\n");
450 push_includes(&mut output, &manifest.c_includes);
451 output.push('\n');
452 push_declarations(&mut output, &manifest.private_declarations)?;
453 let public_functions = public_function_declarations(&manifest.public_declarations);
454 for source_item in &manifest.source_items {
455 match source_item {
456 SourceItem::StaticFragment { path } => {
457 let fragment_content =
458 read_bridge_fragment(manifest_dir, path, "C static fragment")?;
459 if !output.ends_with("\n\n") {
460 output.push('\n');
461 }
462 output.push_str(fragment_content.trim_end());
463 output.push('\n');
464 }
465 SourceItem::Wrapper {
466 function,
467 ffi_symbols,
468 ownership: _,
469 body_fragment,
470 } => {
471 let declaration = public_functions.get(function.as_str()).with_context(|| {
472 format!("wrapper fragment references unknown public function: {function}")
473 })?;
474 let body =
475 read_bridge_fragment(manifest_dir, body_fragment, "wrapper body fragment")?;
476 for ffi_symbol in ffi_symbols {
477 let call_pattern = format!("{ffi_symbol}(");
478 if !body.contains(&call_pattern) {
479 bail!(
480 "wrapper {function} declares FFI symbol {ffi_symbol}, but its body fragment does not call it"
481 );
482 }
483 }
484 if !output.ends_with("\n\n") {
485 output.push('\n');
486 }
487 push_function_definition_signature(
488 &mut output,
489 declaration.return_type,
490 declaration.name,
491 declaration.parameters,
492 );
493 output.push(' ');
494 output.push_str(body.trim());
495 output.push('\n');
496 }
497 }
498 if output.len() > MAX_GENERATED_BRIDGE_SOURCE_BYTES {
499 bail!("generated bridge source exceeds {MAX_GENERATED_BRIDGE_SOURCE_BYTES} bytes");
500 }
501 }
502 Ok(output)
503}
504
505struct FunctionDeclaration<'a> {
506 return_type: &'a str,
507 name: &'a str,
508 parameters: &'a [Parameter],
509}
510
511fn public_function_declarations(
512 declarations: &[Declaration],
513) -> HashMap<&str, FunctionDeclaration<'_>> {
514 declarations
515 .iter()
516 .filter_map(|declaration| match declaration {
517 Declaration::Function {
518 return_type,
519 name,
520 parameters,
521 ..
522 } => Some((
523 name.as_str(),
524 FunctionDeclaration {
525 return_type,
526 name,
527 parameters,
528 },
529 )),
530 _ => None,
531 })
532 .collect()
533}
534
535fn push_includes(output: &mut String, includes: &[String]) {
536 for include in includes {
537 output.push_str("#include ");
538 output.push_str(include);
539 output.push('\n');
540 }
541}
542
543fn push_declarations(output: &mut String, declarations: &[Declaration]) -> Result<()> {
544 for declaration in declarations {
545 push_declaration(output, declaration)?;
546 output.push('\n');
547 }
548 Ok(())
549}
550
551fn push_declaration(output: &mut String, declaration: &Declaration) -> Result<()> {
552 match declaration {
553 Declaration::Enum { name, variants } => {
554 output.push_str("typedef enum ");
555 output.push_str(name);
556 output.push_str(" {\n");
557 for variant in variants {
558 output.push_str(" ");
559 output.push_str(&variant.name);
560 output.push_str(" = ");
561 output.push_str(&variant.value.to_string());
562 output.push_str(",\n");
563 }
564 output.push_str("} ");
565 output.push_str(name);
566 output.push_str(";\n");
567 }
568 Declaration::Struct { name, fields } => {
569 output.push_str("typedef struct ");
570 output.push_str(name);
571 output.push_str(" {\n");
572 for field in fields {
573 output.push_str(" ");
574 push_typed_name(output, &field.ty, &field.name);
575 output.push_str(";\n");
576 }
577 output.push_str("} ");
578 output.push_str(name);
579 output.push_str(";\n");
580 }
581 Declaration::Function {
582 return_type,
583 name,
584 parameters,
585 storage,
586 } => {
587 push_function_declaration(output, return_type, name, parameters, storage)?;
588 }
589 }
590 Ok(())
591}
592
593fn push_function_declaration(
594 output: &mut String,
595 return_type: &str,
596 name: &str,
597 parameters: &[Parameter],
598 storage: &FunctionStorage,
599) -> Result<()> {
600 if matches!(storage, FunctionStorage::Extern) {
601 output.push_str("extern ");
602 }
603 if parameters.is_empty() {
604 output.push_str(return_type);
605 output.push(' ');
606 output.push_str(name);
607 output.push_str("(void);\n");
608 return Ok(());
609 }
610
611 if parameters.len() == 1 {
612 output.push_str(return_type);
613 output.push(' ');
614 output.push_str(name);
615 output.push('(');
616 push_typed_name(output, ¶meters[0].ty, ¶meters[0].name);
617 output.push_str(");\n");
618 return Ok(());
619 }
620
621 output.push_str(return_type);
622 output.push(' ');
623 output.push_str(name);
624 output.push_str("(\n");
625 for (index, parameter) in parameters.iter().enumerate() {
626 output.push_str(" ");
627 push_typed_name(output, ¶meter.ty, ¶meter.name);
628 if index + 1 == parameters.len() {
629 output.push_str(");\n");
630 } else {
631 output.push_str(",\n");
632 }
633 }
634 Ok(())
635}
636
637fn push_function_definition_signature(
638 output: &mut String,
639 return_type: &str,
640 name: &str,
641 parameters: &[Parameter],
642) {
643 if parameters.is_empty() {
644 output.push_str(return_type);
645 output.push(' ');
646 output.push_str(name);
647 output.push_str("(void)");
648 return;
649 }
650
651 if parameters.len() == 1 {
652 output.push_str(return_type);
653 output.push(' ');
654 output.push_str(name);
655 output.push('(');
656 push_typed_name(output, ¶meters[0].ty, ¶meters[0].name);
657 output.push(')');
658 return;
659 }
660
661 output.push_str(return_type);
662 output.push(' ');
663 output.push_str(name);
664 output.push_str("(\n");
665 for (index, parameter) in parameters.iter().enumerate() {
666 output.push_str(" ");
667 push_typed_name(output, ¶meter.ty, ¶meter.name);
668 if index + 1 == parameters.len() {
669 output.push(')');
670 } else {
671 output.push_str(",\n");
672 }
673 }
674}
675
676fn push_typed_name(output: &mut String, ty: &str, name: &str) {
677 if let Some(marker) = ty.find("(*)") {
678 output.push_str(&ty[..marker + 2]);
679 output.push_str(name);
680 output.push_str(&ty[marker + 2..]);
681 } else {
682 output.push_str(ty);
683 if !ty.ends_with('*') {
684 output.push(' ');
685 }
686 output.push_str(name);
687 }
688}
689
690pub fn bootstrap_from_sources(
691 manifest_path: &Path,
692 fragment_dir: &Path,
693 header_path: &Path,
694 c_path: &Path,
695) -> Result<()> {
696 fs::create_dir_all(fragment_dir)
697 .with_context(|| format!("failed to create fragment dir: {}", fragment_dir.display()))?;
698
699 let header = fs::read_to_string(header_path)
700 .with_context(|| format!("failed to read source header: {}", header_path.display()))?;
701 let source = fs::read_to_string(c_path)
702 .with_context(|| format!("failed to read source C file: {}", c_path.display()))?;
703
704 let public_declarations = parse_header_declarations(&header)?;
705 let (c_includes, private_declarations, body) = parse_source_sections(&source)?;
706 let public_function_names = public_declarations
707 .iter()
708 .filter_map(|declaration| match declaration {
709 Declaration::Function { name, .. } => Some(name.as_str()),
710 _ => None,
711 })
712 .collect::<HashSet<_>>();
713 let source_items = split_body_fragments(&body, &public_function_names, fragment_dir)?;
714
715 let manifest = Manifest {
716 header_guard: "VESPER_PLAYER_KIT_BRIDGE_SHIM_H".to_string(),
717 header_includes: vec![
718 "<stdbool.h>".to_string(),
719 "<stddef.h>".to_string(),
720 "<stdint.h>".to_string(),
721 ],
722 c_includes,
723 public_declarations,
724 private_declarations,
725 source_items,
726 };
727 let manifest_content = serde_json::to_string_pretty(&manifest)?;
728 fs::write(manifest_path, format!("{manifest_content}\n"))
729 .with_context(|| format!("failed to write manifest: {}", manifest_path.display()))?;
730 Ok(())
731}
732
733fn split_body_fragments(
734 body: &str,
735 public_function_names: &HashSet<&str>,
736 fragment_dir: &Path,
737) -> Result<Vec<SourceItem>> {
738 let wrapper_dir = fragment_dir.join("fragments/wrappers");
739 fs::create_dir_all(&wrapper_dir).with_context(|| {
740 format!(
741 "failed to create wrapper fragment dir: {}",
742 wrapper_dir.display()
743 )
744 })?;
745 let static_dir = fragment_dir.join("fragments/static");
746 fs::create_dir_all(&static_dir).with_context(|| {
747 format!(
748 "failed to create static fragment dir: {}",
749 static_dir.display()
750 )
751 })?;
752
753 let mut cursor = 0;
754 let mut static_group = String::new();
755 let mut static_group_index = 0usize;
756 let mut source_items = Vec::new();
757 while let Some(item_start) = next_non_whitespace(body, cursor) {
758 let open_brace = find_top_level_open_brace(body, item_start)?;
759 let item_end = find_matching_brace(body, open_brace)? + 1;
760 let item = &body[item_start..item_end];
761 let signature = body[item_start..open_brace].trim();
762 let function_name = top_level_function_name(signature)?;
763 if signature.starts_with("static ") {
764 if !static_group.is_empty() {
765 static_group.push_str("\n\n");
766 }
767 static_group.push_str(item.trim());
768 } else if public_function_names.contains(function_name.as_str()) {
769 flush_static_group(
770 fragment_dir,
771 &mut source_items,
772 &mut static_group,
773 &mut static_group_index,
774 )?;
775 let body_fragment = &body[open_brace..item_end];
776 let fragment_name = format!("fragments/wrappers/{function_name}.c.inc");
777 let fragment_path = fragment_dir.join(&fragment_name);
778 fs::write(&fragment_path, format!("{}\n", body_fragment.trim())).with_context(
779 || {
780 format!(
781 "failed to write wrapper fragment: {}",
782 fragment_path.display()
783 )
784 },
785 )?;
786 source_items.push(SourceItem::Wrapper {
787 function: function_name,
788 ffi_symbols: sorted_player_ffi_symbols(body_fragment),
789 ownership: ownership_notes(body_fragment),
790 body_fragment: fragment_name,
791 });
792 } else {
793 bail!("unknown non-static bridge function in C body: {function_name}");
794 }
795 cursor = item_end;
796 }
797 flush_static_group(
798 fragment_dir,
799 &mut source_items,
800 &mut static_group,
801 &mut static_group_index,
802 )?;
803
804 Ok(source_items)
805}
806
807fn flush_static_group(
808 fragment_dir: &Path,
809 source_items: &mut Vec<SourceItem>,
810 static_group: &mut String,
811 static_group_index: &mut usize,
812) -> Result<()> {
813 if static_group.trim().is_empty() {
814 return Ok(());
815 }
816 *static_group_index += 1;
817 let fragment_name = format!("fragments/static/static-helpers-{static_group_index:02}.c.inc");
818 let fragment_path = fragment_dir.join(&fragment_name);
819 fs::write(&fragment_path, format!("{}\n", static_group.trim())).with_context(|| {
820 format!(
821 "failed to write static helper fragment: {}",
822 fragment_path.display()
823 )
824 })?;
825 source_items.push(SourceItem::StaticFragment {
826 path: fragment_name,
827 });
828 static_group.clear();
829 Ok(())
830}
831
832fn sorted_player_ffi_symbols(input: &str) -> Vec<String> {
833 let mut symbols = HashSet::new();
834 let mut cursor = 0;
835 while let Some(offset) = input[cursor..].find("player_ffi_") {
836 let start = cursor + offset;
837 let end = input[start..]
838 .find(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_'))
839 .map_or(input.len(), |relative| start + relative);
840 if input[end..].starts_with('(') {
841 symbols.insert(input[start..end].to_string());
842 }
843 cursor = end;
844 }
845 let mut symbols = symbols.into_iter().collect::<Vec<_>>();
846 symbols.sort();
847 symbols
848}
849
850fn ownership_notes(input: &str) -> Vec<String> {
851 let mut notes = Vec::new();
852 if input.contains("player_ffi_error_free(") {
853 notes.push("Frees Rust-owned error messages with player_ffi_error_free.".to_string());
854 }
855 if input.contains("player_ffi_preload_command_list_free(") {
856 notes.push(
857 "Frees Rust-owned preload command lists after copying them to runtime DTOs."
858 .to_string(),
859 );
860 }
861 if input.contains("player_ffi_playlist_active_item_free(") {
862 notes.push(
863 "Frees Rust-owned playlist active item strings after copying them to runtime DTOs."
864 .to_string(),
865 );
866 }
867 if input.contains("player_ffi_download_snapshot_free(") {
868 notes.push(
869 "Frees Rust-owned download snapshots after copying them to runtime DTOs.".to_string(),
870 );
871 }
872 if input.contains("player_ffi_download_command_list_free(") {
873 notes.push(
874 "Frees Rust-owned download command lists after copying them to runtime DTOs."
875 .to_string(),
876 );
877 }
878 if input.contains("player_ffi_download_event_list_free(") {
879 notes.push(
880 "Frees Rust-owned download event lists after copying them to runtime DTOs.".to_string(),
881 );
882 }
883 if input.contains("player_ffi_track_preferences_free(") {
884 notes.push(
885 "Frees Rust-owned resolved track preference strings after copying them to runtime DTOs."
886 .to_string(),
887 );
888 }
889 if input.contains("player_ffi_benchmark_report_string_free(") {
890 notes.push(
891 "Forwards benchmark report string release to the Rust FFI free function.".to_string(),
892 );
893 }
894 if input.contains("player_ffi_dash_bridge_string_free(") {
895 notes
896 .push("Forwards DASH bridge string release to the Rust FFI free function.".to_string());
897 }
898 if input.contains("free(") {
899 notes.push("Releases C-owned bridge allocations with free.".to_string());
900 }
901 if input.contains("calloc(") {
902 notes.push(
903 "Allocates copied C bridge DTO arrays with calloc and pairs them with bridge free wrappers."
904 .to_string(),
905 );
906 }
907 notes
908}
909
910fn next_non_whitespace(input: &str, cursor: usize) -> Option<usize> {
911 input[cursor..]
912 .char_indices()
913 .find(|(_, ch)| !ch.is_whitespace())
914 .map(|(offset, _)| cursor + offset)
915}
916
917fn find_top_level_open_brace(input: &str, start: usize) -> Result<usize> {
918 input[start..]
919 .find('{')
920 .map(|offset| start + offset)
921 .with_context(|| format!("could not find function body after byte {start}"))
922}
923
924fn top_level_function_name(signature: &str) -> Result<String> {
925 let open = signature
926 .rfind('(')
927 .with_context(|| format!("function signature missing (: {signature}"))?;
928 let prefix = signature[..open].trim_end();
929 let name_end = prefix.len();
930 let name_start = prefix[..name_end]
931 .rfind(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_'))
932 .map_or(0, |index| index + 1);
933 let name = &prefix[name_start..name_end];
934 if name.is_empty() {
935 bail!("function signature missing name: {signature}");
936 }
937 Ok(name.to_string())
938}
939
940fn find_matching_brace(input: &str, open_brace: usize) -> Result<usize> {
941 let bytes = input.as_bytes();
942 let mut index = open_brace;
943 let mut depth = 0usize;
944 let mut in_line_comment = false;
945 let mut in_block_comment = false;
946 let mut in_string = false;
947 let mut in_char = false;
948 let mut escaped = false;
949
950 while index < bytes.len() {
951 let byte = bytes[index];
952 let next = bytes.get(index + 1).copied();
953
954 if in_line_comment {
955 if byte == b'\n' {
956 in_line_comment = false;
957 }
958 index += 1;
959 continue;
960 }
961 if in_block_comment {
962 if byte == b'*' && next == Some(b'/') {
963 in_block_comment = false;
964 index += 2;
965 } else {
966 index += 1;
967 }
968 continue;
969 }
970 if in_string {
971 if escaped {
972 escaped = false;
973 } else if byte == b'\\' {
974 escaped = true;
975 } else if byte == b'"' {
976 in_string = false;
977 }
978 index += 1;
979 continue;
980 }
981 if in_char {
982 if escaped {
983 escaped = false;
984 } else if byte == b'\\' {
985 escaped = true;
986 } else if byte == b'\'' {
987 in_char = false;
988 }
989 index += 1;
990 continue;
991 }
992
993 if byte == b'/' && next == Some(b'/') {
994 in_line_comment = true;
995 index += 2;
996 continue;
997 }
998 if byte == b'/' && next == Some(b'*') {
999 in_block_comment = true;
1000 index += 2;
1001 continue;
1002 }
1003 if byte == b'"' {
1004 in_string = true;
1005 index += 1;
1006 continue;
1007 }
1008 if byte == b'\'' {
1009 in_char = true;
1010 index += 1;
1011 continue;
1012 }
1013 if byte == b'{' {
1014 depth += 1;
1015 } else if byte == b'}' {
1016 depth = depth
1017 .checked_sub(1)
1018 .with_context(|| format!("unbalanced closing brace at byte {index}"))?;
1019 if depth == 0 {
1020 return Ok(index);
1021 }
1022 }
1023 index += 1;
1024 }
1025
1026 bail!("could not find matching brace after byte {open_brace}")
1027}
1028
1029fn parse_header_declarations(header: &str) -> Result<Vec<Declaration>> {
1030 let mut body = header;
1031 if let Some(index) = body.find("#define VESPER_PLAYER_KIT_BRIDGE_SHIM_H") {
1032 body = &body[index + "#define VESPER_PLAYER_KIT_BRIDGE_SHIM_H".len()..];
1033 }
1034 let mut declarations = Vec::new();
1035 let mut cursor = 0;
1036 while let Some(start) = find_next_declaration_start(body, cursor) {
1037 if body[start..].starts_with("typedef enum ") {
1038 let (declaration, end) = parse_enum_declaration(body, start)?;
1039 declarations.push(declaration);
1040 cursor = end;
1041 } else if body[start..].starts_with("typedef struct ") {
1042 let (declaration, end) = parse_struct_declaration(body, start)?;
1043 declarations.push(declaration);
1044 cursor = end;
1045 } else {
1046 let (declaration, end) = parse_function_prototype(body, start)?;
1047 declarations.push(declaration);
1048 cursor = end;
1049 }
1050 }
1051 Ok(declarations)
1052}
1053
1054pub fn public_function_names(header: &str) -> Result<BTreeSet<String>> {
1056 Ok(parse_header_declarations(header)?
1057 .into_iter()
1058 .filter_map(|declaration| match declaration {
1059 Declaration::Function { name, .. } => Some(name),
1060 Declaration::Enum { .. } | Declaration::Struct { .. } => None,
1061 })
1062 .collect())
1063}
1064
1065fn parse_source_sections(source: &str) -> Result<(Vec<String>, Vec<Declaration>, String)> {
1066 let mut c_includes = Vec::new();
1067 let mut cursor = 0;
1068 let generated_notice_line = format!("/* {GENERATED_NOTICE} */");
1069 if source.starts_with(&generated_notice_line) {
1070 cursor = generated_notice_line.len();
1071 if source[cursor..].starts_with('\n') {
1072 cursor += 1;
1073 }
1074 }
1075 for line in source[cursor..].lines() {
1076 if line.starts_with("#include ") {
1077 c_includes.push(line.trim_start_matches("#include ").to_string());
1078 cursor += line.len() + 1;
1079 } else if line.trim().is_empty() {
1080 cursor += line.len() + 1;
1081 } else {
1082 break;
1083 }
1084 }
1085
1086 let mut declarations = Vec::new();
1087 let mut body_start = cursor;
1088 loop {
1089 let rest = &source[body_start..];
1090 let trimmed = rest.trim_start();
1091 body_start += rest.len() - trimmed.len();
1092 if trimmed.starts_with("typedef enum ") {
1093 let (declaration, end) = parse_enum_declaration(source, body_start)?;
1094 declarations.push(declaration);
1095 body_start = end;
1096 } else if trimmed.starts_with("typedef struct ") {
1097 let (declaration, end) = parse_struct_declaration(source, body_start)?;
1098 declarations.push(declaration);
1099 body_start = end;
1100 } else if trimmed.starts_with("extern ") {
1101 let (declaration, end) = parse_extern_prototype(source, body_start)?;
1102 declarations.push(declaration);
1103 body_start = end;
1104 } else {
1105 break;
1106 }
1107 }
1108
1109 Ok((c_includes, declarations, source[body_start..].to_string()))
1110}
1111
1112fn find_next_declaration_start(input: &str, cursor: usize) -> Option<usize> {
1113 let candidates = [
1114 input[cursor..]
1115 .find("typedef enum ")
1116 .map(|offset| cursor + offset),
1117 input[cursor..]
1118 .find("typedef struct ")
1119 .map(|offset| cursor + offset),
1120 input[cursor..]
1121 .find("\nbool ")
1122 .map(|offset| cursor + offset + 1),
1123 input[cursor..]
1124 .find("\nvoid ")
1125 .map(|offset| cursor + offset + 1),
1126 input[cursor..]
1127 .find("\nuint64_t ")
1128 .map(|offset| cursor + offset + 1),
1129 input[cursor..]
1130 .find("\nchar *")
1131 .map(|offset| cursor + offset + 1),
1132 ];
1133 candidates.into_iter().flatten().min()
1134}
1135
1136fn parse_enum_declaration(input: &str, start: usize) -> Result<(Declaration, usize)> {
1137 let end = find_typedef_declaration_end(input, start)?;
1138 let block = &input[start..end];
1139 let name = text_between(block, "typedef enum ", " {")?
1140 .trim()
1141 .to_string();
1142 let body = text_between(block, "{", "}")?;
1143 let mut variants = Vec::new();
1144 for line in body.lines() {
1145 let line = line.trim().trim_end_matches(',');
1146 if line.is_empty() {
1147 continue;
1148 }
1149 let (name, value) = line
1150 .split_once('=')
1151 .with_context(|| format!("enum variant missing value: {line}"))?;
1152 variants.push(EnumVariant {
1153 name: name.trim().to_string(),
1154 value: value.trim().parse()?,
1155 });
1156 }
1157 Ok((Declaration::Enum { name, variants }, end))
1158}
1159
1160fn parse_struct_declaration(input: &str, start: usize) -> Result<(Declaration, usize)> {
1161 let end = find_typedef_declaration_end(input, start)?;
1162 let block = &input[start..end];
1163 let name = text_between(block, "typedef struct ", " {")?
1164 .trim()
1165 .to_string();
1166 let body = text_between(block, "{", "}")?;
1167 let mut fields = Vec::new();
1168 for line in body.lines() {
1169 let line = line.trim().trim_end_matches(';');
1170 if line.is_empty() {
1171 continue;
1172 }
1173 let field = parse_named_type(line)?;
1174 fields.push(Field {
1175 ty: field.ty,
1176 name: field.name,
1177 });
1178 }
1179 Ok((Declaration::Struct { name, fields }, end))
1180}
1181
1182fn parse_extern_prototype(input: &str, start: usize) -> Result<(Declaration, usize)> {
1183 let end = find_declaration_end(input, start)?;
1184 let block = input[start..end].trim();
1185 let prototype = block
1186 .strip_prefix("extern ")
1187 .context("extern declaration missing extern prefix")?;
1188 parse_function_declaration_text(prototype, FunctionStorage::Extern)
1189 .map(|declaration| (declaration, end))
1190}
1191
1192fn parse_function_prototype(input: &str, start: usize) -> Result<(Declaration, usize)> {
1193 let end = find_declaration_end(input, start)?;
1194 parse_function_declaration_text(input[start..end].trim(), FunctionStorage::Public)
1195 .map(|declaration| (declaration, end))
1196}
1197
1198fn parse_function_declaration_text(
1199 prototype: &str,
1200 storage: FunctionStorage,
1201) -> Result<Declaration> {
1202 let prototype = prototype.trim_end_matches(';').trim();
1203 let open = prototype
1204 .find('(')
1205 .with_context(|| format!("function declaration missing (: {prototype}"))?;
1206 let close = prototype
1207 .rfind(')')
1208 .with_context(|| format!("function declaration missing ): {prototype}"))?;
1209 let head = prototype[..open].trim();
1210 let (return_type, name) = split_type_and_name(head)?;
1211 let params_text = prototype[open + 1..close].trim();
1212 let parameters = if params_text.is_empty() || params_text == "void" {
1213 Vec::new()
1214 } else {
1215 params_text
1216 .split(',')
1217 .map(|param| parse_named_type(param.trim()))
1218 .collect::<Result<Vec<_>>>()?
1219 };
1220 Ok(Declaration::Function {
1221 return_type,
1222 name,
1223 parameters,
1224 storage,
1225 })
1226}
1227
1228fn parse_named_type(value: &str) -> Result<Parameter> {
1229 let (ty, name) = split_type_and_name(value)?;
1230 Ok(Parameter { ty, name })
1231}
1232
1233fn split_type_and_name(value: &str) -> Result<(String, String)> {
1234 let value = value.trim();
1235 if let Some(marker) = value.find("(*") {
1236 let name_start = marker + 2;
1237 let name_end = value[name_start..]
1238 .find(')')
1239 .map(|offset| name_start + offset)
1240 .with_context(|| format!("function pointer declaration is missing ): {value}"))?;
1241 let prefix = value[..marker].trim_end();
1242 let name = value[name_start..name_end].trim().to_string();
1243 let suffix = &value[name_end + 1..];
1244 if prefix.is_empty() || name.is_empty() || suffix.is_empty() {
1245 bail!("function pointer declaration is missing type or name: {value}");
1246 }
1247 return Ok((format!("{prefix} (*){suffix}"), name));
1248 }
1249
1250 let trimmed = value.trim_end();
1251 let name_end = trimmed.len();
1252 let name_start = trimmed[..name_end]
1253 .rfind(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_'))
1254 .map_or(0, |index| index + 1);
1255 let ty = trimmed[..name_start].trim_end().to_string();
1256 let name = trimmed[name_start..name_end].to_string();
1257 if ty.is_empty() || name.is_empty() {
1258 bail!("declaration is missing type or name: {value}");
1259 }
1260 Ok((ty, name))
1261}
1262
1263fn find_declaration_end(input: &str, start: usize) -> Result<usize> {
1264 input[start..]
1265 .find(';')
1266 .map(|offset| start + offset + 1)
1267 .with_context(|| format!("could not find declaration terminator after byte {start}"))
1268}
1269
1270fn find_typedef_declaration_end(input: &str, start: usize) -> Result<usize> {
1271 let closing_brace = input[start..]
1272 .find("\n}")
1273 .map(|offset| start + offset + 1)
1274 .with_context(|| format!("could not find typedef closing brace after byte {start}"))?;
1275 input[closing_brace..]
1276 .find(';')
1277 .map(|offset| closing_brace + offset + 1)
1278 .with_context(|| format!("could not find typedef terminator after byte {start}"))
1279}
1280
1281fn text_between<'a>(input: &'a str, start: &str, end: &str) -> Result<&'a str> {
1282 let start_index = input
1283 .find(start)
1284 .with_context(|| format!("missing start marker: {start}"))?
1285 + start.len();
1286 let end_index = input[start_index..]
1287 .find(end)
1288 .with_context(|| format!("missing end marker: {end}"))?
1289 + start_index;
1290 Ok(&input[start_index..end_index])
1291}
1292
1293#[cfg(test)]
1294mod tests {
1295 use super::*;
1296 use std::time::{SystemTime, UNIX_EPOCH};
1297
1298 fn static_fragment_manifest(path: &str) -> Manifest {
1299 Manifest {
1300 header_guard: "VESPER_PLAYER_KIT_BRIDGE_SHIM_H".to_string(),
1301 header_includes: vec!["<stdbool.h>".to_string()],
1302 c_includes: vec!["\"include/VesperPlayerKitBridgeShim.h\"".to_string()],
1303 public_declarations: Vec::new(),
1304 private_declarations: Vec::new(),
1305 source_items: vec![SourceItem::StaticFragment {
1306 path: path.to_string(),
1307 }],
1308 }
1309 }
1310
1311 #[test]
1312 fn parses_pointer_type_with_name() {
1313 let parameter = parse_named_type("const char *message").unwrap();
1314 assert_eq!(parameter.ty, "const char *");
1315 assert_eq!(parameter.name, "message");
1316 }
1317
1318 #[test]
1319 fn parses_plain_type_with_name() {
1320 let parameter = parse_named_type("uint64_t handle").unwrap();
1321 assert_eq!(parameter.ty, "uint64_t");
1322 assert_eq!(parameter.name, "handle");
1323 }
1324
1325 #[test]
1326 fn emits_multiline_function_prototype() {
1327 let mut output = String::new();
1328 push_function_declaration(
1329 &mut output,
1330 "bool",
1331 "vesper_runtime_example",
1332 &[
1333 Parameter {
1334 ty: "uint64_t".to_string(),
1335 name: "handle".to_string(),
1336 },
1337 Parameter {
1338 ty: "const char *".to_string(),
1339 name: "message".to_string(),
1340 },
1341 ],
1342 &FunctionStorage::Public,
1343 )
1344 .unwrap();
1345 assert_eq!(
1346 output,
1347 "bool vesper_runtime_example(\n uint64_t handle,\n const char *message);\n"
1348 );
1349 }
1350
1351 #[test]
1352 fn parses_function_pointer_field() {
1353 let parameter =
1354 parse_named_type("void (*on_progress)(void *context, float ratio)").unwrap();
1355 assert_eq!(parameter.ty, "void (*)(void *context, float ratio)");
1356 assert_eq!(parameter.name, "on_progress");
1357
1358 let mut output = String::new();
1359 push_typed_name(&mut output, ¶meter.ty, ¶meter.name);
1360 assert_eq!(output, "void (*on_progress)(void *context, float ratio)");
1361 }
1362
1363 #[test]
1364 fn rejects_wrapper_manifest_with_missing_ffi_symbol_call() {
1365 let temp_dir = std::env::temp_dir().join(format!(
1366 "vesper-shim-generator-test-{}",
1367 SystemTime::now()
1368 .duration_since(UNIX_EPOCH)
1369 .unwrap()
1370 .as_nanos()
1371 ));
1372 std::fs::create_dir_all(temp_dir.join("fragments/wrappers")).unwrap();
1373 std::fs::write(
1374 temp_dir.join("fragments/wrappers/vesper_runtime_example.c.inc"),
1375 "{\n return true;\n}\n",
1376 )
1377 .unwrap();
1378
1379 let manifest = Manifest {
1380 header_guard: "VESPER_PLAYER_KIT_BRIDGE_SHIM_H".to_string(),
1381 header_includes: vec!["<stdbool.h>".to_string()],
1382 c_includes: vec!["\"include/VesperPlayerKitBridgeShim.h\"".to_string()],
1383 public_declarations: vec![Declaration::Function {
1384 return_type: "bool".to_string(),
1385 name: "vesper_runtime_example".to_string(),
1386 parameters: Vec::new(),
1387 storage: FunctionStorage::Public,
1388 }],
1389 private_declarations: Vec::new(),
1390 source_items: vec![SourceItem::Wrapper {
1391 function: "vesper_runtime_example".to_string(),
1392 ffi_symbols: vec!["player_ffi_missing".to_string()],
1393 ownership: Vec::new(),
1394 body_fragment: "fragments/wrappers/vesper_runtime_example.c.inc".to_string(),
1395 }],
1396 };
1397
1398 let error = generate_source(&manifest, &temp_dir).unwrap_err();
1399 let _ = std::fs::remove_dir_all(&temp_dir);
1400 assert!(error.to_string().contains("player_ffi_missing"));
1401 }
1402
1403 #[test]
1404 fn rejects_generated_ffi_reference_omitted_from_manifest_symbols() {
1405 let directory = tempfile::tempdir().expect("temporary bridge FFI symbol fixture");
1406 let fragment_directory = directory.path().join("fragments/wrappers");
1407 fs::create_dir_all(&fragment_directory).expect("create bridge wrapper directory");
1408 fs::write(
1409 fragment_directory.join("vesper_runtime_example.c.inc"),
1410 "{\n return player_ffi_missing();\n}\n",
1411 )
1412 .expect("write bridge wrapper with omitted FFI symbol");
1413 let manifest = Manifest {
1414 header_guard: "VESPER_PLAYER_KIT_BRIDGE_SHIM_H".to_string(),
1415 header_includes: vec!["<stdbool.h>".to_string()],
1416 c_includes: vec!["\"include/VesperPlayerKitBridgeShim.h\"".to_string()],
1417 public_declarations: vec![Declaration::Function {
1418 return_type: "bool".to_string(),
1419 name: "vesper_runtime_example".to_string(),
1420 parameters: Vec::new(),
1421 storage: FunctionStorage::Public,
1422 }],
1423 private_declarations: vec![Declaration::Function {
1424 return_type: "bool".to_string(),
1425 name: "player_ffi_missing".to_string(),
1426 parameters: Vec::new(),
1427 storage: FunctionStorage::Extern,
1428 }],
1429 source_items: vec![SourceItem::Wrapper {
1430 function: "vesper_runtime_example".to_string(),
1431 ffi_symbols: Vec::new(),
1432 ownership: Vec::new(),
1433 body_fragment: "fragments/wrappers/vesper_runtime_example.c.inc".to_string(),
1434 }],
1435 };
1436
1437 let error = generate(&manifest, directory.path())
1438 .expect_err("reject generated FFI references omitted from manifest symbols");
1439
1440 assert!(error.to_string().contains("player_ffi_missing"));
1441 assert!(error.to_string().contains("missing from manifest"));
1442 }
1443
1444 #[test]
1445 fn rejects_parent_fragment_path_escape() {
1446 let directory = tempfile::tempdir().expect("temporary bridge fragment boundary");
1447 let manifest_directory = directory.path().join("manifest");
1448 fs::create_dir(&manifest_directory).expect("create bridge manifest directory");
1449 fs::write(directory.path().join("outside.c.inc"), "int outside = 1;\n")
1450 .expect("write outside bridge fragment");
1451
1452 let error = generate_source(
1453 &static_fragment_manifest("../outside.c.inc"),
1454 &manifest_directory,
1455 )
1456 .expect_err("reject a parent bridge fragment path");
1457
1458 assert!(error.to_string().contains("relative fragment path"));
1459 }
1460
1461 #[cfg(unix)]
1462 #[test]
1463 fn rejects_symlinked_fragment() {
1464 use std::os::unix::fs::symlink;
1465
1466 let directory = tempfile::tempdir().expect("temporary bridge symlink boundary");
1467 let manifest_directory = directory.path().join("manifest");
1468 fs::create_dir(&manifest_directory).expect("create bridge manifest directory");
1469 let outside = directory.path().join("outside.c.inc");
1470 fs::write(&outside, "int outside = 1;\n").expect("write outside bridge fragment");
1471 symlink(&outside, manifest_directory.join("linked.c.inc"))
1472 .expect("link outside bridge fragment");
1473
1474 let error = generate_source(
1475 &static_fragment_manifest("linked.c.inc"),
1476 &manifest_directory,
1477 )
1478 .expect_err("reject a symlinked bridge fragment");
1479
1480 assert!(error.to_string().contains("regular non-symlink file"));
1481 }
1482}