Skip to main content

hara_native/wasm_binding/
package.rs

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}