1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
use anyhow::{bail, Context, Result};
use clap::Args;
use std::{
    fs,
    path::{Path, PathBuf},
};
use wac_types::{ExternKind, ItemKind, Package, SubtypeChecker, Types, WorldId};

/// Verifies that a given WebAssembly component targets a world.
#[derive(Args)]
#[clap(disable_version_flag = true)]
pub struct TargetsCommand {
    /// The path to the component.
    #[clap(value_name = "COMPONENT_PATH")]
    pub component: PathBuf,
    /// The path to the WIT definition containing the world to target.
    #[clap(long, value_name = "WIT_PATH")]
    pub wit: PathBuf,
    /// The name of the world to target
    ///
    /// If the wit package only has one world definition, this does not need to be specified.
    #[clap(long)]
    pub world: Option<String>,
}

impl TargetsCommand {
    /// Executes the command.
    pub async fn exec(self) -> Result<()> {
        log::debug!("executing targets command");
        let mut types = Types::default();

        let wit_bytes = encode_wit_as_component(&self.wit)?;
        let wit = Package::from_bytes("wit", None, wit_bytes, &mut types)?;

        let component_bytes = fs::read(&self.component).with_context(|| {
            format!(
                "failed to read file `{path}`",
                path = self.component.display()
            )
        })?;
        let component = Package::from_bytes("component", None, component_bytes, &mut types)?;

        let wit = get_wit_world(&types, wit.ty(), self.world.as_deref())?;

        validate_target(&types, wit, component.ty())?;

        Ok(())
    }
}

/// Gets the selected world from the component encoded WIT package
fn get_wit_world(
    types: &Types,
    top_level_world: WorldId,
    world_name: Option<&str>,
) -> anyhow::Result<WorldId> {
    let top_level_world = &types[top_level_world];
    let world = match world_name {
        Some(world_name) => top_level_world
            .exports
            .get(world_name)
            .with_context(|| format!("wit package did not contain a world named '{world_name}'"))?,
        None if top_level_world.exports.len() == 1 => {
            top_level_world.exports.values().next().unwrap()
        }
        None if top_level_world.exports.len() > 1 => {
            bail!("wit package has multiple worlds, please specify one with the --world flag")
        }
        None => {
            bail!("wit package did not contain a world")
        }
    };
    let ItemKind::Type(wac_types::Type::World(world_id)) = world else {
        // We expect the top-level world to export a world type
        bail!("wit package was not encoded properly")
    };
    let wit_world = &types[*world_id];
    let world = wit_world.exports.values().next();
    let Some(ItemKind::Component(w)) = world else {
        // We expect the nested world type to export a component
        bail!("wit package was not encoded properly")
    };
    Ok(*w)
}

/// Encodes the wit package found at `path` into a component
fn encode_wit_as_component(path: &Path) -> anyhow::Result<Vec<u8>> {
    let mut resolve = wit_parser::Resolve::new();
    let pkg = if path.is_dir() {
        log::debug!(
            "loading WIT package from directory `{path}`",
            path = path.display()
        );

        let (pkg, _) = resolve.push_dir(path)?;
        pkg
    } else {
        let unresolved = wit_parser::UnresolvedPackage::parse_file(path)?;
        resolve.push(unresolved)?
    };
    let encoded = wit_component::encode(Some(true), &resolve, pkg).with_context(|| {
        format!(
            "failed to encode WIT package from `{path}`",
            path = path.display()
        )
    })?;
    Ok(encoded)
}

/// An error in target validation
#[derive(thiserror::Error, miette::Diagnostic, Debug)]
#[diagnostic(code("component does not match wit world"))]
pub enum Error {
    #[error("the target wit does not have an import named `{import}` but the component does")]
    /// The import is not in the target world
    ImportNotInTarget {
        /// The name of the missing target
        import: String,
    },
    #[error("{kind} `{name}` has a mismatched type for targeted wit world")]
    /// An import or export has a mismatched type for the target world.
    TargetMismatch {
        /// The name of the mismatched item
        name: String,
        /// The extern kind of the item
        kind: ExternKind,
        /// The source of the error
        #[source]
        source: anyhow::Error,
    },
    #[error("the targeted wit world requires an export named `{name}` but the component did not export one")]
    /// Missing an export for the target world.
    MissingTargetExport {
        /// The export name.
        name: String,
        /// The expected item kind.
        kind: ItemKind,
    },
}

/// Validate whether the component conforms to the given world
pub fn validate_target(
    types: &Types,
    wit_world_id: WorldId,
    component_world_id: WorldId,
) -> Result<(), Error> {
    let component_world = &types[component_world_id];
    let wit_world = &types[wit_world_id];
    // The interfaces imported implicitly through uses.
    let implicit_imported_interfaces = wit_world.implicit_imported_interfaces(types);
    let mut cache = Default::default();
    let mut checker = SubtypeChecker::new(&mut cache);

    // The output is allowed to import a subset of the world's imports
    checker.invert();
    for (import, item_kind) in component_world.imports.iter() {
        let expected = implicit_imported_interfaces
            .get(import.as_str())
            .or_else(|| wit_world.imports.get(import))
            .ok_or_else(|| Error::ImportNotInTarget {
                import: import.to_owned(),
            })?;

        checker
            .is_subtype(expected.promote(), types, *item_kind, types)
            .map_err(|e| Error::TargetMismatch {
                kind: ExternKind::Import,
                name: import.to_owned(),
                source: e,
            })?;
    }

    checker.revert();

    // The output must export every export in the world
    for (name, expected) in &wit_world.exports {
        let export = component_world.exports.get(name).copied().ok_or_else(|| {
            Error::MissingTargetExport {
                name: name.clone(),
                kind: *expected,
            }
        })?;

        checker
            .is_subtype(export, types, expected.promote(), types)
            .map_err(|e| Error::TargetMismatch {
                kind: ExternKind::Export,
                name: name.clone(),
                source: e,
            })?;
    }

    Ok(())
}