1#![cfg(not(target_arch = "wasm32"))]
2
3use std::collections::BTreeMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::sync::atomic::{AtomicU64, Ordering};
7
8use sha2::{Digest, Sha256};
9
10use crate::kernel::Form;
11
12use super::{
13 direct_inspection_source, direct_interface_skeleton, generate_hta_adapter, inspect_direct,
14 HaraValueType, MemoryBindingPlan, WasmInterface,
15};
16
17mod manifest;
18#[cfg(test)]
19mod tests;
20use manifest::package_document;
21
22pub const DIRECT_WASM_BINDING_SCHEMA: &str = "hara.wasm-binding/0-alpha";
23pub const DIRECT_WASM_CONFORMANCE_SCHEMA: &str = "hara.wasm-conformance/0-alpha";
24pub const DIRECT_WASM_BUILD_PRODUCT_SCHEMA: &str = "hara.wasm-build-product/0-alpha";
25
26const PACKAGE_FILE: &str = "package.edn";
27const INTERFACE_FILE: &str = "interface.hal";
28const BINDINGS_FILE: &str = "bindings.edn";
29const BUILD_PRODUCT_FILE: &str = "hara.build-product.edn";
30const CONFORMANCE_FILE: &str = "conformance/bindings.edn";
31const ADAPTER_FILE: &str = "adapter.wasm";
32const ADAPTER_MANIFEST_FILE: &str = "adapter.edn";
33const GENERATED_VERSION: &str = env!("CARGO_PKG_VERSION");
34
35static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(1);
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum BindingTarget {
39 CoreV1,
40 MemoryV1,
41 HtaV1,
42}
43
44impl BindingTarget {
45 pub fn as_keyword(self) -> &'static str {
46 match self {
47 Self::CoreV1 => "core.v1",
48 Self::MemoryV1 => "memory.v1",
49 Self::HtaV1 => "hta.v1",
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct InspectionArtifact {
56 pub namespace: String,
57 pub module: String,
58 pub interface_source: String,
59 pub inspection_source: String,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct BoundPackage {
64 pub root: PathBuf,
65 pub namespace: String,
66 pub module: String,
67 pub target: BindingTarget,
68 pub module_digest: String,
69 pub interface_digest: String,
70 pub binding_digest: String,
71 pub files: Vec<String>,
72}
73
74pub fn inspect_module(
75 module_path: &Path,
76 namespace: Option<&str>,
77) -> Result<InspectionArtifact, String> {
78 let bytes = read_bytes(module_path, "module")?;
79 let inspection = inspect_direct(&bytes)?;
80 let module = file_name(module_path, "module")?;
81 let namespace = namespace
82 .map(str::to_owned)
83 .unwrap_or_else(|| generated_namespace(&module));
84 let interface_source = direct_interface_skeleton(&namespace, &module, &inspection)?;
85 let inspection_source = direct_inspection_source(&inspection);
86 Ok(InspectionArtifact {
87 namespace,
88 module,
89 interface_source,
90 inspection_source,
91 })
92}
93
94pub fn write_interface_skeleton(
95 module_path: &Path,
96 output_path: &Path,
97 namespace: Option<&str>,
98) -> Result<InspectionArtifact, String> {
99 let artifact = inspect_module(module_path, namespace)?;
100 write_new_file(output_path, artifact.interface_source.as_bytes())?;
101 Ok(artifact)
102}
103
104pub fn bind_package(
105 interface_path: &Path,
106 module_path: &Path,
107 output_root: &Path,
108) -> Result<BoundPackage, String> {
109 if output_root.exists() {
110 return Err(format!(
111 "wasm-binding/output-exists: {}",
112 output_root.display()
113 ));
114 }
115
116 let interface_input = read_text(interface_path, "interface")?;
117 let interface = WasmInterface::parse(&interface_input, &interface_path.display().to_string())?;
118 let module_bytes = read_bytes(module_path, "module")?;
119 let inspection = inspect_direct(&module_bytes)?;
120 let (target, memory_plan) = binding_target(&interface, &inspection)?;
121 let adapter = if target == BindingTarget::HtaV1 && hta_adapter_eligible(&interface) {
122 Some(generate_hta_adapter(&module_bytes, &interface)?)
123 } else {
124 None
125 };
126
127 let canonical_interface = interface.canonical_source();
128 let module_digest = digest(&module_bytes);
129 let interface_digest = digest(canonical_interface.as_bytes());
130 let bindings = match (target, memory_plan.as_ref(), adapter.as_ref()) {
131 (BindingTarget::HtaV1, _, Some(adapter)) => hta_binding_document(
132 &interface,
133 &module_digest,
134 &interface_digest,
135 Some(&adapter.adapter_digest),
136 )?,
137 (BindingTarget::HtaV1, _, None) => {
138 hta_binding_document(&interface, &module_digest, &interface_digest, None)?
139 }
140 (BindingTarget::MemoryV1, Some(plan), _) => plan.canonical_source(),
141 (BindingTarget::CoreV1, None, _) => {
142 direct_binding_document(&interface, &module_digest, &interface_digest)
143 }
144 _ => return Err("wasm-binding/target-invalid: binding target has no plan".into()),
145 };
146 let binding_digest = digest(bindings.as_bytes());
147 let project = project_document(&interface, target, adapter.is_some())?;
148 let conformance = conformance_document(
149 &interface,
150 target,
151 &module_digest,
152 &interface_digest,
153 &binding_digest,
154 )?;
155 let package_identity = package_identity(&interface.namespace);
156 let build_product = build_product_document(
157 &interface,
158 target,
159 &module_digest,
160 &interface_digest,
161 &binding_digest,
162 adapter.as_ref(),
163 );
164
165 let mut files = BTreeMap::<String, Vec<u8>>::new();
166 files.insert(interface.module.clone(), module_bytes);
167 if let Some(adapter) = adapter.as_ref() {
168 files.insert(ADAPTER_FILE.into(), adapter.bytes.clone());
169 files.insert(
170 ADAPTER_MANIFEST_FILE.into(),
171 adapter.manifest.as_bytes().to_vec(),
172 );
173 }
174 files.insert(INTERFACE_FILE.into(), canonical_interface.into_bytes());
175 files.insert(BINDINGS_FILE.into(), bindings.into_bytes());
176 files.insert(BUILD_PRODUCT_FILE.into(), build_product.into_bytes());
177 files.insert(CONFORMANCE_FILE.into(), conformance.into_bytes());
178 files.insert("project.edn".into(), project.into_bytes());
179 let package = package_document(
180 &interface,
181 target,
182 &package_identity,
183 adapter.is_some(),
184 &files,
185 )?;
186 files.insert(PACKAGE_FILE.into(), package.into_bytes());
187 write_atomic_tree(output_root, &files)?;
188
189 Ok(BoundPackage {
190 root: output_root.to_path_buf(),
191 namespace: interface.namespace,
192 module: interface.module,
193 target,
194 module_digest,
195 interface_digest,
196 binding_digest,
197 files: files.keys().cloned().collect(),
198 })
199}
200
201fn binding_target(
202 interface: &WasmInterface,
203 inspection: &super::DirectWasmInspection,
204) -> Result<(BindingTarget, Option<MemoryBindingPlan>), String> {
205 if interface.hta_required() {
206 Ok((BindingTarget::HtaV1, None))
207 } else if interface.memory.is_some() {
208 let plan = interface.memory_plan()?;
209 plan.verify(inspection)?;
210 Ok((BindingTarget::MemoryV1, Some(plan)))
211 } else if interface.exports.iter().any(|export| export.asynchronous) {
212 super::verify_hta_scalar(interface, inspection)?;
213 Ok((BindingTarget::HtaV1, None))
214 } else {
215 interface.verify_direct(inspection)?;
216 Ok((BindingTarget::CoreV1, None))
217 }
218}
219
220fn hta_adapter_eligible(interface: &WasmInterface) -> bool {
221 interface.memory.is_none()
222 && interface.capabilities.is_empty()
223 && interface.host_calls.is_empty()
224 && interface.callbacks.is_empty()
225 && interface.handles.is_empty()
226 && interface.resources.is_empty()
227 && interface.exports.iter().all(|export| {
228 export.errors.is_none()
229 && export.returns.hara_type.direct_wasm_type().is_some()
230 && export
231 .arguments
232 .iter()
233 .all(|argument| argument.hara_type.direct_wasm_type().is_some())
234 })
235}
236
237fn project_document(
238 interface: &WasmInterface,
239 target: BindingTarget,
240 has_adapter: bool,
241) -> Result<String, String> {
242 let exports = interface
243 .exports
244 .iter()
245 .map(|export| {
246 let arguments = export
247 .arguments
248 .iter()
249 .map(|argument| manifest_type(&argument.hara_type))
250 .collect::<Result<Vec<_>, _>>()?;
251 let mut fields = vec![
252 (
253 keyword_form("wasm/export"),
254 string_form(&export.wasm_export),
255 ),
256 (keyword_form("args"), Form::Vector(arguments)),
257 (
258 keyword_form("returns"),
259 manifest_type(&export.returns.hara_type)?,
260 ),
261 (keyword_form("async"), Form::Bool(export.asynchronous)),
262 ];
263 if let Some(operation) = export.operation.as_ref() {
264 fields.push((keyword_form("operation"), string_form(operation)));
265 }
266 Ok((string_form(&export.name), Form::Map(fields)))
267 })
268 .collect::<Result<Vec<_>, String>>()?;
269 let mut assets = vec![
270 string_form(INTERFACE_FILE),
271 string_form(BINDINGS_FILE),
272 string_form(BUILD_PRODUCT_FILE),
273 string_form(CONFORMANCE_FILE),
274 ];
275 if target == BindingTarget::HtaV1 {
276 assets.push(string_form(&interface.module));
277 if has_adapter {
278 assets.push(string_form(ADAPTER_MANIFEST_FILE));
279 }
280 }
281 let module = if target == BindingTarget::HtaV1 && has_adapter {
282 ADAPTER_FILE
283 } else {
284 &interface.module
285 };
286 let extension = Form::Map(vec![
287 (
288 keyword_form("identity"),
289 string_form(&package_identity(&interface.namespace)),
290 ),
291 (keyword_form("provider"), keyword_form("wasm")),
292 (keyword_form("module"), string_form(module)),
293 (keyword_form("abi"), keyword_form(target.as_keyword())),
294 (keyword_form("exports"), Form::Map(exports)),
295 (
296 keyword_form("capabilities"),
297 Form::Vector(
298 interface
299 .capabilities
300 .iter()
301 .map(|capability| keyword_form(capability))
302 .collect(),
303 ),
304 ),
305 (keyword_form("host-calls"), host_calls_form(interface)),
306 (keyword_form("callbacks"), callbacks_form(interface)?),
307 (keyword_form("handles"), handles_form(interface)),
308 (keyword_form("assets"), Form::Vector(assets)),
309 ]);
310 let project_id = format!("generated/{}", interface.namespace.replace('.', "-"));
311 Ok(document(Form::Map(vec![
312 (keyword_form("hara/type"), keyword_form("project")),
313 (keyword_form("hara/version"), string_form("1.0.0")),
314 (keyword_form("project/id"), symbol_form(&project_id)),
315 (
316 keyword_form("project/version"),
317 string_form(GENERATED_VERSION),
318 ),
319 (
320 keyword_form("project/source-paths"),
321 Form::Vector(Vec::new()),
322 ),
323 (keyword_form("project/test-paths"), Form::Vector(Vec::new())),
324 (
325 keyword_form("project/extension-paths"),
326 Form::Vector(Vec::new()),
327 ),
328 (keyword_form("project/capabilities"), Form::Set(Vec::new())),
329 (
330 keyword_form("project/extensions"),
331 Form::Map(vec![(symbol_form(&interface.namespace), extension)]),
332 ),
333 ])))
334}
335
336fn direct_binding_document(
337 interface: &WasmInterface,
338 module_digest: &str,
339 interface_digest: &str,
340) -> String {
341 document(Form::Map(vec![
342 (
343 keyword_form("schema"),
344 string_form(DIRECT_WASM_BINDING_SCHEMA),
345 ),
346 (keyword_form("target"), keyword_form("core.v1")),
347 (keyword_form("namespace"), symbol_form(&interface.namespace)),
348 (
349 keyword_form("module"),
350 Form::Map(vec![
351 (keyword_form("path"), string_form(&interface.module)),
352 (keyword_form("digest"), string_form(module_digest)),
353 ]),
354 ),
355 (
356 keyword_form("interface"),
357 Form::Map(vec![
358 (keyword_form("path"), string_form(INTERFACE_FILE)),
359 (keyword_form("digest"), string_form(interface_digest)),
360 ]),
361 ),
362 (
363 keyword_form("exports"),
364 Form::Vector(
365 interface
366 .exports
367 .iter()
368 .map(direct_export_contract)
369 .collect(),
370 ),
371 ),
372 ]))
373}
374
375fn hta_binding_document(
376 interface: &WasmInterface,
377 module_digest: &str,
378 interface_digest: &str,
379 adapter_digest: Option<&str>,
380) -> Result<String, String> {
381 let exports = interface
382 .exports
383 .iter()
384 .map(public_export_contract)
385 .collect::<Result<Vec<_>, _>>()?;
386 let mut entries = vec![
387 (
388 keyword_form("schema"),
389 string_form(DIRECT_WASM_BINDING_SCHEMA),
390 ),
391 (keyword_form("target"), keyword_form("hta.v1")),
392 (keyword_form("namespace"), symbol_form(&interface.namespace)),
393 (
394 keyword_form("module"),
395 Form::Map(vec![
396 (keyword_form("path"), string_form(&interface.module)),
397 (keyword_form("digest"), string_form(module_digest)),
398 ]),
399 ),
400 (
401 keyword_form("interface"),
402 Form::Map(vec![
403 (keyword_form("path"), string_form(INTERFACE_FILE)),
404 (keyword_form("digest"), string_form(interface_digest)),
405 ]),
406 ),
407 (keyword_form("exports"), Form::Vector(exports)),
408 ];
409 if let Some(adapter_digest) = adapter_digest {
410 entries.insert(
411 5,
412 (
413 keyword_form("adapter"),
414 Form::Map(vec![
415 (keyword_form("path"), string_form(ADAPTER_FILE)),
416 (keyword_form("digest"), string_form(adapter_digest)),
417 ]),
418 ),
419 );
420 }
421 Ok(document(Form::Map(entries)))
422}
423
424fn conformance_document(
425 interface: &WasmInterface,
426 target: BindingTarget,
427 module_digest: &str,
428 interface_digest: &str,
429 binding_digest: &str,
430) -> Result<String, String> {
431 let exports = interface
432 .exports
433 .iter()
434 .map(public_export_contract)
435 .collect::<Result<Vec<_>, _>>()?;
436 Ok(document(Form::Map(vec![
437 (
438 keyword_form("schema"),
439 string_form(DIRECT_WASM_CONFORMANCE_SCHEMA),
440 ),
441 (keyword_form("target"), keyword_form(target.as_keyword())),
442 (keyword_form("namespace"), symbol_form(&interface.namespace)),
443 (keyword_form("module-digest"), string_form(module_digest)),
444 (
445 keyword_form("interface-digest"),
446 string_form(interface_digest),
447 ),
448 (keyword_form("binding-digest"), string_form(binding_digest)),
449 (keyword_form("exports"), Form::Vector(exports)),
450 ])))
451}
452
453fn build_product_document(
454 interface: &WasmInterface,
455 target: BindingTarget,
456 module_digest: &str,
457 interface_digest: &str,
458 binding_digest: &str,
459 adapter: Option<&super::AdapterArtifact>,
460) -> String {
461 let product_type = if target == BindingTarget::HtaV1 && adapter.is_some() {
462 "hta-adapter-wasm"
463 } else if target == BindingTarget::HtaV1 {
464 "hta-wasm-module"
465 } else {
466 "extension-wasm-module"
467 };
468 let artifact_path = if target == BindingTarget::HtaV1 && adapter.is_some() {
469 ADAPTER_FILE
470 } else {
471 &interface.module
472 };
473 let mut inputs = vec![
474 (keyword_form("module-digest"), string_form(module_digest)),
475 (
476 keyword_form("interface-digest"),
477 string_form(interface_digest),
478 ),
479 ];
480 if let Some(adapter) = adapter {
481 inputs.push((
482 keyword_form("adapter-digest"),
483 string_form(&adapter.adapter_digest),
484 ));
485 inputs.push((
486 keyword_form("adapter-manifest-digest"),
487 string_form(&digest(adapter.manifest.as_bytes())),
488 ));
489 }
490 let mut files = vec![
491 PACKAGE_FILE,
492 "project.edn",
493 interface.module.as_str(),
494 INTERFACE_FILE,
495 BINDINGS_FILE,
496 BUILD_PRODUCT_FILE,
497 CONFORMANCE_FILE,
498 ];
499 if adapter.is_some() {
500 files.push(ADAPTER_FILE);
501 files.push(ADAPTER_MANIFEST_FILE);
502 }
503 document(Form::Map(vec![
504 (
505 keyword_form("schema"),
506 string_form(DIRECT_WASM_BUILD_PRODUCT_SCHEMA),
507 ),
508 (keyword_form("product/type"), keyword_form(product_type)),
509 (
510 keyword_form("product/namespace"),
511 symbol_form(&interface.namespace),
512 ),
513 (
514 keyword_form("product/target"),
515 keyword_form(target.as_keyword()),
516 ),
517 (
518 keyword_form("product/tool"),
519 Form::Map(vec![
520 (keyword_form("name"), string_form("hara-wasm-bindgen")),
521 (
522 keyword_form("version"),
523 string_form(env!("CARGO_PKG_VERSION")),
524 ),
525 ]),
526 ),
527 (keyword_form("product/inputs"), Form::Map(inputs)),
528 (
529 keyword_form("product/binding-digest"),
530 string_form(binding_digest),
531 ),
532 (
533 keyword_form("product/files"),
534 Form::Vector(files.into_iter().map(string_form).collect()),
535 ),
536 (keyword_form("product/artifact"), string_form(artifact_path)),
537 ]))
538}
539
540fn direct_export_contract(export: &super::BindingFunction) -> Form {
541 Form::Map(vec![
542 (keyword_form("hara/name"), symbol_form(&export.name)),
543 (
544 keyword_form("wasm/export"),
545 string_form(&export.wasm_export),
546 ),
547 (
548 keyword_form("arguments"),
549 Form::Vector(
550 export
551 .arguments
552 .iter()
553 .map(|argument| keyword_form(argument.wasm_type.as_keyword()))
554 .collect(),
555 ),
556 ),
557 (
558 keyword_form("returns"),
559 keyword_form(export.returns.wasm_type.as_keyword()),
560 ),
561 ])
562}
563
564fn public_export_contract(export: &super::BindingFunction) -> Result<Form, String> {
565 let arguments = export
566 .arguments
567 .iter()
568 .map(|argument| manifest_type(&argument.hara_type))
569 .collect::<Result<Vec<_>, _>>()?;
570 let mut fields = vec![
571 (keyword_form("hara/name"), symbol_form(&export.name)),
572 (
573 keyword_form("wasm/export"),
574 string_form(&export.wasm_export),
575 ),
576 (keyword_form("arguments"), Form::Vector(arguments)),
577 (
578 keyword_form("returns"),
579 manifest_type(&export.returns.hara_type)?,
580 ),
581 ];
582 if let Some(operation) = export.operation.as_ref() {
583 fields.push((keyword_form("operation"), string_form(operation)));
584 }
585 Ok(Form::Map(fields))
586}
587
588fn host_calls_form(interface: &WasmInterface) -> Form {
589 Form::Map(
590 interface
591 .host_calls
592 .iter()
593 .map(|(service, contract)| {
594 let mut fields = vec![(
595 keyword_form("methods"),
596 Form::Vector(
597 contract
598 .methods
599 .iter()
600 .map(|method| string_form(method))
601 .collect(),
602 ),
603 )];
604 if !contract.capabilities.is_empty() {
605 fields.push((
606 keyword_form("capabilities"),
607 Form::Vector(
608 contract
609 .capabilities
610 .iter()
611 .map(|capability| keyword_form(capability))
612 .collect(),
613 ),
614 ));
615 }
616 (
617 string_form(service),
618 Form::Map(fields),
619 )
620 })
621 .collect(),
622 )
623}
624
625fn handles_form(interface: &WasmInterface) -> Form {
626 let mut handles = interface.handles.clone();
627 handles.extend(
628 interface
629 .resources
630 .iter()
631 .map(|(name, contract)| (name.clone(), contract.clone())),
632 );
633 Form::Map(
634 handles
635 .iter()
636 .map(|(name, contract)| {
637 let mut fields = vec![(keyword_form("tag"), symbol_form(&contract.tag))];
638 if let Some(release) = contract.release.as_deref() {
639 fields.push((keyword_form("release"), string_form(release)));
640 }
641 (string_form(name), Form::Map(fields))
642 })
643 .collect(),
644 )
645}
646
647fn callbacks_form(interface: &WasmInterface) -> Result<Form, String> {
648 Ok(Form::Map(
649 interface
650 .callbacks
651 .iter()
652 .map(|(name, contract)| {
653 let mut fields = vec![
654 (
655 keyword_form("args"),
656 Form::Vector(
657 contract
658 .arguments
659 .iter()
660 .map(|argument| manifest_type(&argument.hara_type))
661 .collect::<Result<Vec<_>, _>>()?,
662 ),
663 ),
664 (
665 keyword_form("returns"),
666 manifest_type(&contract.returns)?,
667 ),
668 ];
669 fields.push((keyword_form("reentrant"), Form::Bool(contract.reentrant)));
670 Ok((string_form(name), Form::Map(fields)))
671 })
672 .collect::<Result<Vec<_>, String>>()?,
673 ))
674}
675
676fn manifest_type(value: &HaraValueType) -> Result<Form, String> {
677 let name = match value {
678 HaraValueType::I32 => "i32",
679 HaraValueType::I64 => "i64",
680 HaraValueType::F32 => "f32",
681 HaraValueType::F64 => "f64",
682 HaraValueType::Boolean => "boolean",
683 HaraValueType::String => "string",
684 HaraValueType::Bytes => "bytes",
685 HaraValueType::Void => "void",
686 HaraValueType::Record(name) => return Ok(named_type_form("record", name)),
687 HaraValueType::Variant(name) => return Ok(named_type_form("variant", name)),
688 HaraValueType::Handle(name) => return Ok(named_type_form("handle", name)),
689 HaraValueType::Callback(name) => return Ok(named_type_form("callback", name)),
690 };
691 Ok(keyword_form(name))
692}
693
694fn named_type_form(kind: &str, name: &str) -> Form {
695 Form::Vector(vec![keyword_form(kind), symbol_form(name)])
696}
697
698fn write_atomic_tree(root: &Path, files: &BTreeMap<String, Vec<u8>>) -> Result<(), String> {
699 let parent = root.parent().unwrap_or_else(|| Path::new("."));
700 fs::create_dir_all(parent).map_err(|error| {
701 format!(
702 "wasm-binding/output-unavailable: {} ({error})",
703 parent.display()
704 )
705 })?;
706 let name = root
707 .file_name()
708 .and_then(|value| value.to_str())
709 .unwrap_or("package");
710 let temp = parent.join(format!(
711 ".{name}.hara-bind-{}-{}",
712 std::process::id(),
713 TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed)
714 ));
715 if temp.exists() {
716 fs::remove_dir_all(&temp).map_err(|error| {
717 format!(
718 "wasm-binding/output-unavailable: {} ({error})",
719 temp.display()
720 )
721 })?;
722 }
723
724 let result = (|| {
725 fs::create_dir(&temp).map_err(|error| {
726 format!(
727 "wasm-binding/output-unavailable: {} ({error})",
728 temp.display()
729 )
730 })?;
731 for (relative, bytes) in files {
732 let target = temp.join(relative);
733 if let Some(parent) = target.parent() {
734 fs::create_dir_all(parent).map_err(|error| {
735 format!(
736 "wasm-binding/output-unavailable: {} ({error})",
737 parent.display()
738 )
739 })?;
740 }
741 fs::write(&target, bytes).map_err(|error| {
742 format!(
743 "wasm-binding/output-unavailable: {} ({error})",
744 target.display()
745 )
746 })?;
747 }
748 fs::rename(&temp, root).map_err(|error| {
749 format!(
750 "wasm-binding/output-unavailable: {} ({error})",
751 root.display()
752 )
753 })?;
754 Ok(())
755 })();
756 if result.is_err() {
757 let _ = fs::remove_dir_all(&temp);
758 }
759 result
760}
761
762fn write_new_file(path: &Path, bytes: &[u8]) -> Result<(), String> {
763 if path.exists() {
764 return Err(format!("wasm-binding/output-exists: {}", path.display()));
765 }
766 if let Some(parent) = path.parent() {
767 fs::create_dir_all(parent).map_err(|error| {
768 format!(
769 "wasm-binding/output-unavailable: {} ({error})",
770 parent.display()
771 )
772 })?;
773 }
774 let temp = path.with_extension(format!(
775 "hara-bind-{}-{}",
776 std::process::id(),
777 TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed)
778 ));
779 fs::write(&temp, bytes).map_err(|error| {
780 format!(
781 "wasm-binding/output-unavailable: {} ({error})",
782 temp.display()
783 )
784 })?;
785 if let Err(error) = fs::rename(&temp, path) {
786 let _ = fs::remove_file(&temp);
787 return Err(format!(
788 "wasm-binding/output-unavailable: {} ({error})",
789 path.display()
790 ));
791 }
792 Ok(())
793}
794
795fn read_bytes(path: &Path, subject: &str) -> Result<Vec<u8>, String> {
796 fs::read(path).map_err(|error| {
797 format!(
798 "wasm-binding/input-unavailable: {subject} {} ({error})",
799 path.display()
800 )
801 })
802}
803
804fn read_text(path: &Path, subject: &str) -> Result<String, String> {
805 fs::read_to_string(path).map_err(|error| {
806 format!(
807 "wasm-binding/input-unavailable: {subject} {} ({error})",
808 path.display()
809 )
810 })
811}
812
813fn file_name(path: &Path, subject: &str) -> Result<String, String> {
814 path.file_name()
815 .and_then(|value| value.to_str())
816 .filter(|value| !value.is_empty())
817 .map(str::to_owned)
818 .ok_or_else(|| {
819 format!("wasm-binding/input-unavailable: {subject} path has no UTF-8 file name")
820 })
821}
822
823fn generated_namespace(module: &str) -> String {
824 let stem = module.strip_suffix(".wasm").unwrap_or(module);
825 let mut component = String::new();
826 let mut separated = false;
827 for character in stem.chars() {
828 if character.is_ascii_alphanumeric() {
829 if separated && !component.is_empty() {
830 component.push('-');
831 }
832 component.push(character.to_ascii_lowercase());
833 separated = false;
834 } else {
835 separated = true;
836 }
837 }
838 if component.is_empty() {
839 component.push_str("module");
840 }
841 format!("generated.{component}")
842}
843
844fn package_identity(namespace: &str) -> String {
845 format!("generated/{}", namespace.replace('.', "-"))
846}
847
848fn digest(bytes: &[u8]) -> String {
849 format!("sha256:{:x}", Sha256::digest(bytes))
850}
851
852fn document(form: Form) -> String {
853 format!("{form}\n")
854}
855
856fn keyword_form(value: &str) -> Form {
857 Form::Keyword(value.to_owned())
858}
859
860fn symbol_form(value: &str) -> Form {
861 Form::Symbol(value.to_owned())
862}
863
864fn string_form(value: &str) -> Form {
865 Form::String(value.to_owned())
866}