Repository: bytecodealliance/wasm-tools (rust)

Issue:
wasmparser: ComponentNameParser deviates from CM spec
From the current [explainer](https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#import-and-export-definitions):

```enbf
exportname    ::= <plainname>
                | <interfacename>
importname    ::= <exportname>
                | <depname>
                | <urlname>
                | <hashname>
plainname     ::= <label>
                | '[constructor]' <label>
                | '[method]' <label> '.' <label>
                | '[static]' <label> '.' <label>
label         ::= <word>
                | <label> '-' <word>
word          ::= [a-z] [0-9a-z]*
                | [A-Z] [0-9A-Z]*
interfacename ::= <namespace> <label> <projection> <version>?
                | <namespace>+ <label> <projection>+ <version>? 🪺
namespace     ::= <label> ':'
projection    ::= '/' <label>
version       ::= '@' <valid semver>
depname       ::= 'unlocked-dep=<' <pkgnamequery> '>'
                | 'locked-dep=<' <pkgname> '>' ( ',' <hashname> )?
pkgnamequery  ::= <pkgpath> <verrange>?
pkgname       ::= <pkgpath> <version>?
pkgpath       ::= <namespace> <label>
                | <namespace>+ <label> <projection>* 🪺
verrange      ::= '@*'
                | '@{' <verlower> '}'
                | '@{' <verupper> '}'
                | '@{' <verlower> ' ' <verupper> '}'
verlower      ::= '>=' <valid semver>
verupper      ::= '<' <valid semver>
urlname       ::= 'url=<' <nonbrackets> '>' (',' <hashname>)?
                | 'relative-url=<' <nonbrackets> '>' (',' <hashname>)?
nonbrackets   ::= [^<>]*
hashname      ::= 'integrity=<' <integrity-metadata> '>'
```

There are several deviations from this currently implemented in `wasmparser` name validation, chiefly:

* `interfacename` should allow for multiple namespaces and multiple projections.
* `pkgpath` (via `unlocked-dep` and `locked-dep`) should allow for multiple namespaces and multiple projections.
* `urlname` should ensure the URL doesn't contain `<` (we validate that it doesn't contain `>` as we treat that as a terminating character).

Currently the parsing of `unlocked-dep` and `locked-dep` isn't allowing for projections, which is needed to allow tools to specify package dependency information on its imports.

Pull request:
Update import name validation in `wasmparser`.
This PR updates the import name validation in wasmparser to conform to the current component-model spec.

It allows for nested namespaces in interface, locked dep, and unlock dep names.

Additionally, locked and unlocked dep names can now contain one or more projections.

Interface names can now have more than one projection.

A check was added to ensure url and relative-url names don't contain a `<`.

Fixes #1314.

Patch:
diff --git a/crates/wasm-shrink/src/lib.rs b/crates/wasm-shrink/src/lib.rs
index 63315f73..05b1bea7 100755
--- a/crates/wasm-shrink/src/lib.rs
+++ b/crates/wasm-shrink/src/lib.rs
@@ -236,6 +236,7 @@ impl ShrinkRun {
             function_references: false,
             gc: false,
             component_model_values: false,
+            component_model_nested_names: false,
 
             floats: true,
             memory_control: true,
diff --git a/crates/wasmparser/benches/benchmark.rs b/crates/wasmparser/benches/benchmark.rs
index 24660923..e5cdd4b4 100644
--- a/crates/wasmparser/benches/benchmark.rs
+++ b/crates/wasmparser/benches/benchmark.rs
@@ -251,28 +251,7 @@ fn define_benchmarks(c: &mut Criterion) {
     let _ = env_logger::try_init();
 
     fn validator() -> Validator {
-        Validator::new_with_features(WasmFeatures {
-            reference_types: true,
-            multi_value: true,
-            simd: true,
-            relaxed_simd: true,
-            exceptions: true,
-            component_model: true,
-            bulk_memory: true,
-            threads: true,
-            tail_call: true,
-            multi_memory: true,
-            memory64: true,
-            extended_const: true,
-            floats: true,
-            mutable_global: true,
-            saturating_float_to_int: true,
-            sign_extension: true,
-            function_references: true,
-            memory_control: true,
-            gc: true,
-            component_model_values: true,
-        })
+        Validator::new_with_features(WasmFeatures::all())
     }
 
     let test_inputs = once_cell::unsync::Lazy::new(collect_benchmark_inputs);
diff --git a/crates/wasmparser/src/validator.rs b/crates/wasmparser/src/validator.rs
index 12beb465..e6309354 100644
--- a/crates/wasmparser/src/validator.rs
+++ b/crates/wasmparser/src/validator.rs
@@ -249,6 +249,8 @@ pub struct WasmFeatures {
     pub gc: bool,
     /// Support for the `value` type in the component model proposal.
     pub component_model_values: bool,
+    /// Support for the nested namespaces and projects in component model names.
+    pub component_model_nested_names: bool,
 }
 
 impl WasmFeatures {
@@ -275,6 +277,7 @@ impl WasmFeatures {
             memory_control: true,
             gc: true,
             component_model_values: true,
+            component_model_nested_names: true,
         }
     }
 
@@ -365,6 +368,7 @@ impl Default for WasmFeatures {
             memory_control: false,
             gc: false,
             component_model_values: false,
+            component_model_nested_names: false,
 
             // On-by-default features (phase 4 or greater).
             mutable_global: true,
diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs
index 808e9392..b05609d2 100644
--- a/crates/wasmparser/src/validator/component.rs
+++ b/crates/wasmparser/src/validator/component.rs
@@ -448,6 +448,7 @@ impl ComponentState {
             &mut self.import_names,
             &mut self.imports,
             &mut self.type_info,
+            features,
         )?;
         Ok(())
     }
@@ -941,6 +942,7 @@ impl ComponentState {
             &mut self.export_names,
             &mut self.exports,
             &mut self.type_info,
+            features,
         )?;
         Ok(())
     }
@@ -2128,6 +2130,7 @@ impl ComponentState {
                 &mut export_names,
                 &mut inst_exports,
                 &mut info,
+                features,
             )?;
         }
 
@@ -2993,10 +2996,11 @@ impl ComponentNameContext {
         kind_names: &mut IndexSet<ComponentName>,
         items: &mut IndexMap<String, ComponentEntityType>,
         info: &mut TypeInfo,
+        features: &WasmFeatures,
     ) -> Result<()> {
         // First validate that `name` is even a valid kebab name, meaning it's
         // in kebab-case, is an ID, etc.
-        let kebab = ComponentName::new(name, offset)
+        let kebab = ComponentName::new_with_features(name, offset, *features)
             .with_context(|| format!("{} name `{name}` is not a valid extern name", kind.desc()))?;
 
         if let ExternKind::Export = kind {
diff --git a/crates/wasmparser/src/validator/names.rs b/crates/wasmparser/src/validator/names.rs
index 863e05b2..79627230 100644
--- a/crates/wasmparser/src/validator/names.rs
+++ b/crates/wasmparser/src/validator/names.rs
@@ -1,7 +1,7 @@
 //! Definitions of name-related helpers and newtypes, primarily for the
 //! component model.
 
-use crate::Result;
+use crate::{Result, WasmFeatures};
 use semver::Version;
 use std::borrow::Borrow;
 use std::fmt;
@@ -273,7 +273,27 @@ impl ComponentName {
     /// Attempts to parse `name` as a valid component name, returning `Err` if
     /// it's not valid.
     pub fn new(name: &str, offset: usize) -> Result<ComponentName> {
-        let mut parser = ComponentNameParser { next: name, offset };
+        Self::new_with_features(
+            name,
+            offset,
+            WasmFeatures {
+                component_model: true,
+                ..Default::default()
+            },
+        )
+    }
+
+    /// Attempts to parse `name` as a valid component name, returning `Err` if
+    /// it's not valid.
+    ///
+    /// `features` can be used to enable or disable validation of certain forms
+    /// of supported import names.
+    pub fn new_with_features(name: &str, offset: usize, features: WasmFeatures) -> Result<Self> {
+        let mut parser = ComponentNameParser {
+            next: name,
+            offset,
+            features,
+        };
         let kind = parser.parse()?;
         if !parser.next.is_empty() {
             bail!(offset, "trailing characters found: `{}`", parser.next);
@@ -414,27 +434,34 @@ impl<'a> InterfaceName<'a> {
         self.0
     }
 
-    /// Returns the `a` in `a:b/c`
+    /// Returns the `a:b` in `a:b:c/d/e`
     pub fn namespace(&self) -> &'a KebabStr {
-        let colon = self.0.find(':').unwrap();
+        let colon = self.0.rfind(':').unwrap();
         KebabStr::new_unchecked(&self.0[..colon])
     }
 
-    /// Returns the `b` in `a:b/c`
+    /// Returns the `c` in `a:b:c/d/e`
     pub fn package(&self) -> &'a KebabStr {
-        let colon = self.0.find(':').unwrap();
+        let colon = self.0.rfind(':').unwrap();
         let slash = self.0.find('/').unwrap();
         KebabStr::new_unchecked(&self.0[colon + 1..slash])
     }
 
-    /// Returns the `c` in `a:b/c`
+    /// Returns the `d` in `a:b:c/d/e`.
     pub fn interface(&self) -> &'a KebabStr {
+        let projection = self.projection();
+        let slash = projection.find('/').unwrap_or(projection.len());
+        KebabStr::new_unchecked(&projection[..slash])
+    }
+
+    /// Returns the `d/e` in `a:b:c/d/e`
+    pub fn projection(&self) -> &'a KebabStr {
         let slash = self.0.find('/').unwrap();
         let at = self.0.find('@').unwrap_or(self.0.len());
         KebabStr::new_unchecked(&self.0[slash + 1..at])
     }
 
-    /// Returns the `1.2.3` in `a:b/c@1.2.3`
+    /// Returns the `1.2.3` in `a:b:c/d/e@1.2.3`
     pub fn version(&self) -> Option<Version> {
         let at = self.0.find('@')?;
         Some(Version::parse(&self.0[at + 1..]).unwrap())
@@ -484,6 +511,7 @@ impl<'a> HashName<'a> {
 struct ComponentNameParser<'a> {
     next: &'a str,
     offset: usize,
+    features: WasmFeatures,
 }
 
 impl<'a> ComponentNameParser<'a> {
@@ -505,18 +533,18 @@ impl<'a> ComponentNameParser<'a> {
             return Ok(ParsedComponentNameKind::Static);
         }
 
-        // 'unlocked-dep=<' <pkgidset> '>'
+        // 'unlocked-dep=<' <pkgnamequery> '>'
         if self.eat_str("unlocked-dep=") {
             self.expect_str("<")?;
-            self.pkgidset_up_to('>')?;
+            self.pkg_name_query()?;
             self.expect_str(">")?;
             return Ok(ParsedComponentNameKind::Dependency);
         }
 
-        // 'locked-dep=<' <pkgid> '>' ( ',' <hashname> )?
+        // 'locked-dep=<' <pkgname> '>' ( ',' <hashname> )?
         if self.eat_str("locked-dep=") {
             self.expect_str("<")?;
-            self.pkgid_up_to('>')?;
+            self.pkg_name(false)?;
             self.expect_str(">")?;
             self.eat_optional_hash()?;
             return Ok(ParsedComponentNameKind::Dependency);
@@ -525,7 +553,10 @@ impl<'a> ComponentNameParser<'a> {
         // 'url=<' <nonbrackets> '>' (',' <hashname>)?
         if self.eat_str("url=") {
             self.expect_str("<")?;
-            let _url = self.take_up_to('>')?;
+            let url = self.take_up_to('>')?;
+            if url.contains('<') {
+                bail!(self.offset, "url cannot contain `<`");
+            }
             self.expect_str(">")?;
             self.eat_optional_hash()?;
             return Ok(ParsedComponentNameKind::Url);
@@ -533,7 +564,10 @@ impl<'a> ComponentNameParser<'a> {
         // 'relative-url=<' <nonbrackets> '>' (',' <hashname>)?
         if self.eat_str("relative-url=") {
             self.expect_str("<")?;
-            let _url = self.take_up_to('>')?;
+            let url = self.take_up_to('>')?;
+            if url.contains('<') {
+                bail!(self.offset, "relative-url cannot contain `<`");
+            }
             self.expect_str(">")?;
             self.eat_optional_hash()?;
             return Ok(ParsedComponentNameKind::Url);
@@ -547,98 +581,120 @@ impl<'a> ComponentNameParser<'a> {
             return Ok(ParsedComponentNameKind::Hash);
         }
 
-        match self.eat_until(':') {
-            // interfacename ::= <namespace> <label> <projection> <version>?
-            Some(namespace) => {
-                self.kebab(namespace)?;
-                let pkg = self.take_until('/')?;
-                self.kebab(pkg)?;
-                match self.eat_until('@') {
-                    Some(interface) => {
-                        self.kebab(interface)?;
-                        let version = self.take_rest();
-                        self.semver(version)?;
-                    }
-                    None => {
-                        self.expect_kebab()?;
-                    }
-                }
-                Ok(ParsedComponentNameKind::Interface)
-            }
-            None => {
-                self.expect_kebab()?;
-                Ok(ParsedComponentNameKind::Label)
-            }
+        if self.next.contains(':') {
+            self.pkg_name(true)?;
+            Ok(ParsedComponentNameKind::Interface)
+        } else {
+            self.expect_kebab()?;
+            Ok(ParsedComponentNameKind::Label)
         }
     }
 
-    // pkgidset      ::= <pkgname> <verrange>?
-    // pkgname       ::= <namespace> <label>
-    // verrange      ::= '@*'
-    //                 | '@{' <verlower> '}'
-    //                 | '@{' <verupper> '}'
-    //                 | '@{' <verlower> ' ' <verupper> '}'
-    // verlower      ::= '>=' <valid semver>
-    // verupper      ::= '<' <valid semver>
-    fn pkgidset_up_to(&mut self, end: char) -> Result<()> {
-        let namespace = self.take_until(':')?;
-        self.kebab(namespace)?;
-        let name = match self.eat_until('@') {
-            Some(name) => name,
-            // a:b
-            None => {
-                let name = self.take_up_to(end)?;
-                self.kebab(name)?;
+    // pkgnamequery ::= <pkgpath> <verrange>?
+    fn pkg_name_query(&mut self) -> Result<()> {
+        self.pkg_path(false)?;
+
+        if self.eat_str("@") {
+            if self.eat_str("*") {
                 return Ok(());
             }
-        };
-        self.kebab(name)?;
 
-        // a:b@*
-        if self.eat_str("*") {
-            return Ok(());
+            self.expect_str("{")?;
+            let range = self.take_up_to('}')?;
+            self.expect_str("}")?;
+            self.semver_range(range)?;
         }
-        self.expect_str("{")?;
-        if self.eat_str(">=") {
-            match self.eat_until(' ') {
-                Some(lower) => {
-                    self.semver(lower)?;
-                }
-                // a:b@{>=1.2.3}
-                None => {
-                    let lower = self.take_until('}')?;
-                    self.semver(lower)?;
-                    return Ok(());
+
+        Ok(())
+    }
+
+    // pkgname ::= <pkgpath> <version>?
+    fn pkg_name(&mut self, require_projection: bool) -> Result<()> {
+        self.pkg_path(require_projection)?;
+
+        if self.eat_str("@") {
+            let version = match self.eat_up_to('>') {
+                Some(version) => version,
+                None => self.take_rest(),
+            };
+
+            self.semver(version)?;
+        }
+
+        Ok(())
+    }
+
+    // pkgpath ::= <namespace>+ <label> <projection>*
+    fn pkg_path(&mut self, require_projection: bool) -> Result<()> {
+        // There must be at least one package namespace
+        self.take_kebab()?;
+        self.expect_str(":")?;
+        self.take_kebab()?;
+
+        if self.features.component_model_nested_names {
+            // Take the remaining package namespaces and name
+            while self.next.starts_with(':') {
+                self.expect_str(":")?;
+                self.take_kebab()?;
+            }
+        }
+
+        // Take the projections
+        if self.next.starts_with('/') {
+            self.expect_str("/")?;
+            self.take_kebab()?;
+
+            if self.features.component_model_nested_names {
+                while self.next.starts_with('/') {
+                    self.expect_str("/")?;
+                    self.take_kebab()?;
                 }
             }
+        } else if require_projection {
+            bail!(self.offset, "expected `/` after package name");
         }
 
-        // a:b@{<1.2.3}
-        // .. or
-        // a:b@{<1.2.3 >=1.2.3}
-        self.expect_str("<")?;
-        let version = self.take_until('}')?;
-        self.semver(version)?;
         Ok(())
     }
 
-    // pkgid         ::= <pkgname> <version>?
-    fn pkgid_up_to(&mut self, end: char) -> Result<()> {
-        let namespace = self.take_until(':')?;
-        self.kebab(namespace)?;
-        match self.eat_until('@') {
-            // a:b@1.2.3
-            Some(name) => {
-                self.kebab(name)?;
-                let version = self.take_up_to(end)?;
-                self.semver(version)?;
-            }
-            // a:b
-            None => {
-                let name = self.take_up_to(end)?;
-                self.kebab(name)?;
+    // verrange ::= '@*'
+    //            | '@{' <verlower> '}'
+    //            | '@{' <verupper> '}'
+    //            | '@{' <verlower> ' ' <verupper> '}'
+    // verlower ::= '>=' <valid semver>
+    // verupper ::= '<' <valid semver>
+    fn semver_range(&self, range: &str) -> Result<()> {
+        if range == "*" {
+            return Ok(());
+        }
+
+        if let Some(range) = range.strip_prefix(">=") {
+            let (lower, upper) = range
+                .split_once(' ')
+                .map(|(l, u)| (l, Some(u)))
+                .unwrap_or((range, None));
+            self.semver(lower)?;
+
+            if let Some(upper) = upper {
+                match upper.strip_prefix('<') {
+                    Some(upper) => {
+                        self.semver(upper)?;
+                    }
+                    None => bail!(
+                        self.offset,
+                        "expected `<` at start of version range upper bounds"
+                    ),
+                }
             }
+        } else if let Some(upper) = range.strip_prefix('<') {
+            self.semver(upper)?;
+        } else {
+            bail!(
+                self.offset,
+                "expected `>=` or `<` at start of version range"
+            );
         }
+
         Ok(())
     }
 
@@ -750,6 +806,17 @@ impl<'a> ComponentNameParser<'a> {
         ret
     }
 
+    fn take_kebab(&mut self) -> Result<&'a KebabStr> {
+        self.next
+            .find(|c| !matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-'))
+            .map(|i| {
+                let (kebab, next) = self.next.split_at(i);
+                self.next = next;
+                self.kebab(kebab)
+            })
+            .unwrap_or_else(|| self.expect_kebab())
+    }
+
     fn expect_kebab(&mut self) -> Result<&'a KebabStr> {
         let s = self.take_rest();
         self.kebab(s)
diff --git a/crates/wit-component/src/decoding.rs b/crates/wit-component/src/decoding.rs
index 2f1057de..03aee249 100644
--- a/crates/wit-component/src/decoding.rs
+++ b/crates/wit-component/src/decoding.rs
@@ -36,6 +36,9 @@ enum WitEncodingVersion {
 impl<'a> ComponentInfo<'a> {
     /// Creates a new component info by parsing the given WebAssembly component bytes.
     fn new(bytes: &'a [u8]) -> Result<Self> {
+        if !Parser::is_component(bytes) {
+            bail!("input is not a component wasm binary");
+        }
         let mut validator = Validator::new_with_features(WasmFeatures::all());
         let mut externs = Vec::new();
         let mut depth = 1;
diff --git a/crates/wit-component/src/lib.rs b/crates/wit-component/src/lib.rs
index 0b6930f4..57356451 100644
--- a/crates/wit-component/src/lib.rs
+++ b/crates/wit-component/src/lib.rs
@@ -83,8 +83,18 @@ pub(crate) fn base_producers() -> wasm_metadata::Producers {
     producer
 }
 
-/// Parse a WIT file from a path that represents a top level 'wit' directory,
-/// normally containing a 'deps' folder.
+/// Parse a WIT package from the input `path`.
+///
+/// The input `path` can be one of:
+///
+/// * A directory containing a WIT package with an optional `deps` directory for
+///   any dependent WIT packages it references.
+/// * A single standalone WIT file with no dependencies.
+/// * A wasm-encoded WIT package as a single file in the wasm binary format.
+/// * A wasm-encoded WIT package as a single file in the wasm text format.
+///
+/// The `Resolve` containing WIT information along with the `PackageId` of what
+/// was parsed is returned if successful.
 pub fn parse_wit_from_path(
     path: impl AsRef<std::path::Path>,
 ) -> Result<(Resolve, wit_parser::PackageId)> {
diff --git a/fuzz/src/validate.rs b/fuzz/src/validate.rs
index 81edcdf9..13859dc6 100644
--- a/fuzz/src/validate.rs
+++ b/fuzz/src/validate.rs
@@ -24,6 +24,7 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> {
         function_references: u.arbitrary()?,
         gc: u.arbitrary()?,
         component_model_values: u.arbitrary()?,
+        component_model_nested_names: u.arbitrary()?,
     });
     let use_maybe_invalid = u.arbitrary()?;
 
diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs
index 85c0edee..7a4a09de 100644
--- a/src/bin/wasm-tools/component.rs
+++ b/src/bin/wasm-tools/component.rs
@@ -441,56 +441,7 @@ impl WitOpts {
 
     /// Executes the application.
     fn run(self) -> Result<()> {
-        // First up determine the actual `DecodedWasm` as the input. This could
-        // come from a number of sources:
-        //
-        // * If a `*.wat` or `*.wasm` is specified, use `wit_component::decode`
-        // * If a directory is specified, parse it as a `Resolve`-oriented
-        //   package with a `deps` directory optionally available.
-        // * If a file is specified then it's just a normal wit package where
-        //   deps can't be resolved.
-        // * If no file is specified then parse the input as either `*.wat`,
-        //   `*.wasm`, or `*.wit` and do as above.
-        //
-        // Eventually there will want to be more flags for things like
-        // specifying a directory but specifying the WIT dependencies are
-        // located elsewhere. This should be sufficient for now though.
-        let decoded = match &self.input {
-            Some(input) => match input.extension().and_then(|s| s.to_str()) {
-                Some("wat") | Some("wasm") => {
-                    let bytes = wat::parse_file(&input)?;
-                    decode_wasm(&bytes).context("failed to decode WIT document")?
-                }
-                _ => {
-                    let (resolve, id) = parse_wit_from_path(input)?;
-                    DecodedWasm::WitPackage(resolve, id)
-                }
-            },
-            None => {
-                let mut stdin = Vec::new();
-                std::io::stdin()
-                    .read_to_end(&mut stdin)
-                    .context("failed to read <stdin>")?;
-
-                if is_wasm_binary_or_wat(&stdin) {
-                    let bytes = wat::parse_bytes(&stdin).map_err(|mut e| {
-                        e.set_path("<stdin>");
-                        e
-                    })?;
-
-                    decode_wasm(&bytes).context("failed to decode WIT document")?
-                } else {
-                    let stdin = match std::str::from_utf8(&stdin) {
-                        Ok(s) => s,
-                        Err(_) => bail!("stdin was not valid utf-8"),
-                    };
-                    let mut resolve = Resolve::default();
-                    let pkg = UnresolvedPackage::parse("<stdin>".as_ref(), stdin)?;
-                    let id = resolve.push(pkg)?;
-                    DecodedWasm::WitPackage(resolve, id)
-                }
-            }
-        };
+        let decoded = self.decode_input()?;
 
         // Now that the WIT document has been decoded, it's time to emit it.
         // This interprets all of the output options and performs such a task.
@@ -506,6 +457,63 @@ impl WitOpts {
         Ok(())
     }
 
+    fn decode_input(&self) -> Result<DecodedWasm> {
+        // If the input is a directory then it's probably raw WIT files, so use
+        // `parse_wit_from_path`.
+        if let Some(input) = &self.input {
+            if input.is_dir() {
+                let (resolve, id) = parse_wit_from_path(input)?;
+                return Ok(DecodedWasm::WitPackage(resolve, id));
+            }
+        }
+
+        // ... otherwise if the input is not a directory then it's read into
+        // memory here and decoded below. Note that this specifically does not
+        // use `parse_wit_from_path` because this wants to additionally handle
+        // the input case that the input is a core wasm binary with a
+        // `component-type` section inside of it.
+        let (input, path) = match &self.input {
+            Some(input) => (
+                std::fs::read(input).with_context(|| format!("failed to read {input:?}"))?,
+                input.as_path(),
+            ),
+            None => {
+                let mut stdin = Vec::new();
+                std::io::stdin()
+                    .read_to_end(&mut stdin)
+                    .context("failed to read <stdin>")?;
+                (stdin, Path::new("<stdin>"))
+            }
+        };
+
+        if is_wasm_binary_or_wat(&input) {
+            // Use `wat` to possible translate the text format, and then
+            // afterwards use either `decode` or `metadata::decode` depending on
+            // if the input is a component or a core wasm mdoule.
+            let input = wat::parse_bytes(&input).map_err(|mut e| {
+                e.set_path(path);
+                e
+            })?;
+            if wasmparser::Parser::is_component(&input) {
+                wit_component::decode(&input)
+            } else {
+                let (_wasm, bindgen) = wit_component::metadata::decode(&input)?;
+                Ok(DecodedWasm::Component(bindgen.resolve, bindgen.world))
+            }
+        } else {
+            // This is a single WIT file, so create the single-file package and
+            // return it.
+            let input = match std::str::from_utf8(&input) {
+                Ok(s) => s,
+                Err(_) => bail!("input was not valid utf-8"),
+            };
+            let mut resolve = Resolve::default();
+            let pkg = UnresolvedPackage::parse(path, input)?;
+            let id = resolve.push(pkg)?;
+            Ok(DecodedWasm::WitPackage(resolve, id))
+        }
+    }
+
     fn emit_wasm(&self, decoded: &DecodedWasm) -> Result<()> {
         assert!(self.wasm || self.wat);
         assert!(self.out_dir.is_none());
@@ -634,12 +642,3 @@ impl TargetsOpts {
         Ok(())
     }
 }
-
-fn decode_wasm(bytes: &[u8]) -> Result<DecodedWasm> {
-    if wasmparser::Parser::is_component(bytes) {
-        wit_component::decode(bytes)
-    } else {
-        let (_wasm, bindgen) = wit_component::metadata::decode(bytes)?;
-        Ok(DecodedWasm::Component(bindgen.resolve, bindgen.world))
-    }
-}


Test patch:
diff --git a/crates/wasm-smith/tests/available_imports.rs b/crates/wasm-smith/tests/available_imports.rs
index 9d12a2af..9d3acbd8 100644
--- a/crates/wasm-smith/tests/available_imports.rs
+++ b/crates/wasm-smith/tests/available_imports.rs
@@ -179,6 +179,7 @@ fn parser_features_from_config(config: &impl Config) -> WasmFeatures {
         memory_control: false,
         gc: false,
         component_model_values: false,
+        component_model_nested_names: false,
     }
 }
 
diff --git a/crates/wasm-smith/tests/core.rs b/crates/wasm-smith/tests/core.rs
index a33c50aa..edf1f2b0 100644
--- a/crates/wasm-smith/tests/core.rs
+++ b/crates/wasm-smith/tests/core.rs
@@ -152,6 +152,7 @@ fn parser_features_from_config(config: &impl Config) -> WasmFeatures {
         memory_control: false,
         gc: false,
         component_model_values: false,
+        component_model_nested_names: false,
     }
 }
 
diff --git a/crates/wit-component/tests/linking.rs b/crates/wit-component/tests/linking.rs
index 36f63cd2..b774827d 100644
--- a/crates/wit-component/tests/linking.rs
+++ b/crates/wit-component/tests/linking.rs
@@ -1,7 +1,6 @@
 use {
     anyhow::{Context, Result},
-    std::{borrow::Cow, path::Path},
-    wasm_encoder::{CustomSection, Encode as _, Section as _},
+    std::path::Path,
     wit_component::StringEncoding,
     wit_parser::{Resolve, UnresolvedPackage},
 };
@@ -144,21 +143,13 @@ fn encode(wat: &str, wit: Option<&str>) -> Result<Vec<u8>> {
         let mut resolve = Resolve::default();
         let pkg = resolve.push(UnresolvedPackage::parse(Path::new("wit"), wit)?)?;
         let world = resolve.select_world(pkg, None)?;
-        let component_type = wit_component::metadata::encode(
+
+        wit_component::embed_component_metadata(
+            &mut module,
             &resolve,
             world,
             StringEncoding::UTF8,
-            None,
-            Some(true),
         )?;
-
-        let section = CustomSection {
-            name: Cow::Borrowed("component-type"),
-            data: Cow::Borrowed(&component_type),
-        };
-
-        module.push(section.id());
-        section.encode(&mut module);
     }
 
     wasmparser::validate(&module)?;
diff --git a/tests/local/component-model/import-extended.wast b/tests/local/component-model/import-extended.wast
new file mode 100644
index 00000000..042facd1
--- /dev/null
+++ b/tests/local/component-model/import-extended.wast
@@ -0,0 +1,25 @@
+;; These are the extended import name forms that are currently supported
+;; via WasmFeatures::component_model_nested_names.
+
+(component
+  (import "a:b:c:d/e" (func))
+  (import "a:b-c:d-e:f-g/h-i/j-k/l-m/n/o/p@1.0.0" (func))
+)
+
+(component
+  (import "unlocked-dep=<a:b:c:d/e/f/g>" (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g@*>" (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g@{>=1.2.3}>" (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g@{>=1.2.3-rc}>" (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g@{<1.2.3}>" (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g@{<1.2.3-rc}>" (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g@{>=1.2.3 <1.2.3}>" (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g@{>=1.2.3-rc <1.2.3}>" (func))
+)
+
+(component
+  (import "locked-dep=<a:b:c:d/e/f/g>" (func))
+  (import "locked-dep=<a:b:c:d/e/f/g@1.2.3>" (func))
+  (import "locked-dep=<a:b:c:d/e/f/g>,integrity=<sha256-a>" (func))
+  (import "locked-dep=<a:b:c:d/e/f/g@1.2.3>,integrity=<sha256-a>" (func))
+)
diff --git a/tests/local/component-model/import.wast b/tests/local/component-model/import.wast
index 485a9a73..d95bfec7 100644
--- a/tests/local/component-model/import.wast
+++ b/tests/local/component-model/import.wast
@@ -133,7 +133,7 @@
   "`` is not in kebab case")
 (assert_invalid
   (component (import "wasi:" (func)))
-  "failed to find `/` character")
+  "`` is not in kebab case")
 (assert_invalid
   (component (import "wasi:/" (func)))
   "not in kebab case")
@@ -176,13 +176,18 @@
 (assert_invalid
   (component (import "wasi:http/types@2.0.0-" (func)))
   "empty identifier segment")
+(assert_invalid
+  (component (import "foo:bar:baz/qux" (func)))
+  "expected `/` after package name")
+(assert_invalid
+  (component (import "foo:bar/baz/qux" (func)))
+  "trailing characters found: `/qux`")
 
 (component
   (import "a" (func $a))
   (export "a" (func $a))
 )
 
-
 (component
   (import "unlocked-dep=<a:b>" (func))
   (import "unlocked-dep=<a:b@*>" (func))
@@ -199,10 +204,10 @@
   "expected `<` at ``")
 (assert_invalid
   (component (import "unlocked-dep=<" (func)))
-  "failed to find `:`")
+  "`` is not in kebab case")
 (assert_invalid
   (component (import "unlocked-dep=<>" (func)))
-  "failed to find `:`")
+  "`` is not in kebab case")
 (assert_invalid
   (component (import "unlocked-dep=<:>" (func)))
   "`` is not in kebab case")
@@ -217,7 +222,7 @@
   "expected `{` at `>`")
 (assert_invalid
   (component (import "unlocked-dep=<a:a@{xyz}>" (func)))
-  "expected `<` at `xyz}>`")
+  "expected `>=` or `<` at start of version range")
 (assert_invalid
   (component (import "unlocked-dep=<a:a@{<xyz}>" (func)))
   "`xyz` is not a valid semver")
@@ -237,28 +242,28 @@
   "expected `<` at ``")
 (assert_invalid
   (component (import "locked-dep=<" (func)))
-  "failed to find `:`")
+  "`` is not in kebab case")
 (assert_invalid
   (component (import "locked-dep=<:" (func)))
-  "is not in kebab case")
+  "`` is not in kebab case")
 (assert_invalid
   (component (import "locked-dep=<:>" (func)))
-  "is not in kebab case")
+  "`` is not in kebab case")
 (assert_invalid
   (component (import "locked-dep=<a:>" (func)))
-  "is not in kebab case")
+  "`` is not in kebab case")
 (assert_invalid
   (component (import "locked-dep=<:a>" (func)))
-  "is not in kebab case")
+  "`` is not in kebab case")
 (assert_invalid
   (component (import "locked-dep=<a:a" (func)))
-  "failed to find `>`")
+  "expected `>` at ``")
 (assert_invalid
   (component (import "locked-dep=<a:a@>" (func)))
   "is not a valid semver")
 (assert_invalid
   (component (import "locked-dep=<a:a@1.2.3" (func)))
-  "failed to find `>`")
+  "expected `>` at ``")
 (assert_invalid
   (component (import "locked-dep=<a:a@1.2.3>," (func)))
   "expected `integrity=<`")
@@ -278,6 +283,25 @@
 (assert_invalid
   (component (import "url=<" (func)))
   "failed to find `>`")
+(assert_invalid
+  (component (import "url=<<>" (func)))
+  "url cannot contain `<`")
+
+(component
+  (import "relative-url=<>" (func))
+  (import "relative-url=<a>" (func))
+  (import "relative-url=<a>,integrity=<sha256-a>" (func))
+)
+
+(assert_invalid
+  (component (import "relative-url=" (func)))
+  "expected `<` at ``")
+(assert_invalid
+  (component (import "relative-url=<" (func)))
+  "failed to find `>`")
+(assert_invalid
+  (component (import "relative-url=<<>" (func)))
+  "relative-url cannot contain `<`")
 
 (component
   (import "integrity=<sha256-a>" (func))
diff --git a/tests/roundtrip.rs b/tests/roundtrip.rs
index 48da681a..a60ee812 100644
--- a/tests/roundtrip.rs
+++ b/tests/roundtrip.rs
@@ -629,6 +629,7 @@ impl TestState {
             memory_control: true,
             gc: true,
             component_model_values: true,
+            component_model_nested_names: false,
         };
         for part in test.iter().filter_map(|t| t.to_str()) {
             match part {
@@ -678,6 +679,9 @@ impl TestState {
                     features.function_references = true;
                     features.gc = true;
                 }
+                "import-extended.wast" => {
+                    features.component_model_nested_names = true;
+                }
                 _ => {}
             }
         }
diff --git a/tests/snapshots/local/component-model/import-extended.wast/0.print b/tests/snapshots/local/component-model/import-extended.wast/0.print
new file mode 100644
index 00000000..3eb9dbe5
--- /dev/null
+++ b/tests/snapshots/local/component-model/import-extended.wast/0.print
@@ -0,0 +1,6 @@
+(component
+  (type (;0;) (func))
+  (import "a:b:c:d/e" (func (;0;) (type 0)))
+  (type (;1;) (func))
+  (import "a:b-c:d-e:f-g/h-i/j-k/l-m/n/o/p@1.0.0" (func (;1;) (type 1)))
+)
\ No newline at end of file
diff --git a/tests/snapshots/local/component-model/import-extended.wast/1.print b/tests/snapshots/local/component-model/import-extended.wast/1.print
new file mode 100644
index 00000000..bcc9d8bd
--- /dev/null
+++ b/tests/snapshots/local/component-model/import-extended.wast/1.print
@@ -0,0 +1,18 @@
+(component
+  (type (;0;) (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g>" (func (;0;) (type 0)))
+  (type (;1;) (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g@*>" (func (;1;) (type 1)))
+  (type (;2;) (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g@{>=1.2.3}>" (func (;2;) (type 2)))
+  (type (;3;) (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g@{>=1.2.3-rc}>" (func (;3;) (type 3)))
+  (type (;4;) (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g@{<1.2.3}>" (func (;4;) (type 4)))
+  (type (;5;) (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g@{<1.2.3-rc}>" (func (;5;) (type 5)))
+  (type (;6;) (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g@{>=1.2.3 <1.2.3}>" (func (;6;) (type 6)))
+  (type (;7;) (func))
+  (import "unlocked-dep=<a:b:c:d/e/f/g@{>=1.2.3-rc <1.2.3}>" (func (;7;) (type 7)))
+)
\ No newline at end of file
diff --git a/tests/snapshots/local/component-model/import-extended.wast/2.print b/tests/snapshots/local/component-model/import-extended.wast/2.print
new file mode 100644
index 00000000..df142dfb
--- /dev/null
+++ b/tests/snapshots/local/component-model/import-extended.wast/2.print
@@ -0,0 +1,10 @@
+(component
+  (type (;0;) (func))
+  (import "locked-dep=<a:b:c:d/e/f/g>" (func (;0;) (type 0)))
+  (type (;1;) (func))
+  (import "locked-dep=<a:b:c:d/e/f/g@1.2.3>" (func (;1;) (type 1)))
+  (type (;2;) (func))
+  (import "locked-dep=<a:b:c:d/e/f/g>,integrity=<sha256-a>" (func (;2;) (type 2)))
+  (type (;3;) (func))
+  (import "locked-dep=<a:b:c:d/e/f/g@1.2.3>,integrity=<sha256-a>" (func (;3;) (type 3)))
+)
\ No newline at end of file
diff --git a/tests/snapshots/local/component-model/import.wast/32.print b/tests/snapshots/local/component-model/import.wast/34.print
similarity index 100%
rename from tests/snapshots/local/component-model/import.wast/32.print
rename to tests/snapshots/local/component-model/import.wast/34.print
diff --git a/tests/snapshots/local/component-model/import.wast/33.print b/tests/snapshots/local/component-model/import.wast/35.print
similarity index 100%
rename from tests/snapshots/local/component-model/import.wast/33.print
rename to tests/snapshots/local/component-model/import.wast/35.print
diff --git a/tests/snapshots/local/component-model/import.wast/44.print b/tests/snapshots/local/component-model/import.wast/46.print
similarity index 100%
rename from tests/snapshots/local/component-model/import.wast/44.print
rename to tests/snapshots/local/component-model/import.wast/46.print
diff --git a/tests/snapshots/local/component-model/import.wast/56.print b/tests/snapshots/local/component-model/import.wast/58.print
similarity index 100%
rename from tests/snapshots/local/component-model/import.wast/56.print
rename to tests/snapshots/local/component-model/import.wast/58.print
diff --git a/tests/snapshots/local/component-model/import.wast/62.print b/tests/snapshots/local/component-model/import.wast/62.print
new file mode 100644
index 00000000..65a9eb74
--- /dev/null
+++ b/tests/snapshots/local/component-model/import.wast/62.print
@@ -0,0 +1,8 @@
+(component
+  (type (;0;) (func))
+  (import "relative-url=<>" (func (;0;) (type 0)))
+  (type (;1;) (func))
+  (import "relative-url=<a>" (func (;1;) (type 1)))
+  (type (;2;) (func))
+  (import "relative-url=<a>,integrity=<sha256-a>" (func (;2;) (type 2)))
+)
\ No newline at end of file