Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions crates/wasm-encoder/src/component/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,34 @@ pub struct ComponentExternName<'a> {
/// An optional `(implements ...)` directive (See 🏷️ in the component model
/// explainer).
pub implements: Option<Cow<'a, str>>,
/// An optional `(versionsuffix ...)` directive (See 🔗 in the component
/// model explainer).
pub version_suffix: Option<Cow<'a, str>>,
/// An optional `(external-id ...)` directive (See 🏷️ in the component model
/// explainer).
pub external_id: Option<Cow<'a, str>>,
}

impl Encode for ComponentExternName<'_> {
fn encode(&self, bytes: &mut Vec<u8>) {
let mut options = Vec::new();

if let Some(s) = &self.implements {
let ComponentExternName {
name: _,
implements,
version_suffix,
external_id,
} = self;

if let Some(s) = implements {
options.push((0x00, s.as_bytes()));
}
if let Some(s) = version_suffix {
options.push((0x01, s.as_bytes()));
}
if let Some(s) = external_id {
options.push((0x02, s.as_bytes()));
}

if options.is_empty() {
// Prior to WebAssembly/component-model#263 import and export names
Expand Down Expand Up @@ -213,16 +232,15 @@ impl<'a> From<&'a str> for ComponentExternName<'a> {
ComponentExternName {
name: Cow::Borrowed(name),
implements: None,
external_id: None,
version_suffix: None,
}
}
}

impl<'a> From<&'a String> for ComponentExternName<'a> {
fn from(name: &'a String) -> Self {
ComponentExternName {
name: Cow::Borrowed(name),
implements: None,
}
ComponentExternName::from(name.as_str())
}
}

Expand All @@ -231,17 +249,26 @@ impl<'a> From<String> for ComponentExternName<'a> {
ComponentExternName {
name: Cow::Owned(name),
implements: None,
external_id: None,
version_suffix: None,
}
}
}

#[cfg(feature = "wasmparser")]
impl<'a> From<wasmparser::ComponentExternName<'a>> for ComponentExternName<'a> {
fn from(name: wasmparser::ComponentExternName<'a>) -> Self {
let wasmparser::ComponentExternName { name, implements } = name;
let wasmparser::ComponentExternName {
name,
implements,
external_id,
version_suffix,
} = name;
ComponentExternName {
name: name.into(),
implements: implements.map(|s| s.into()),
external_id: external_id.map(|s| s.into()),
version_suffix: version_suffix.map(|s| s.into()),
}
}
}
6 changes: 6 additions & 0 deletions crates/wasmparser/src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,12 @@ define_wasm_features! {
/// Corresponds to the 🏷️ character in
/// <https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md>.
pub cm_implements: CM_IMPLEMENTS(1 << 40) = false;

/// Support for the `(versionsuffix "...")` directive in the component model
///
/// Corresponds to the 🔗 character in
/// <https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md>.
pub cm_canon_names: CM_CANON_NAMES(1 << 41) = false;
}
}

Expand Down
22 changes: 21 additions & 1 deletion crates/wasmparser/src/readers/component/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ pub type ComponentImportSectionReader<'a> = SectionLimited<'a, ComponentImport<'
pub struct ComponentExternName<'a> {
pub name: &'a str,
pub implements: Option<&'a str>,
pub version_suffix: Option<&'a str>,
pub external_id: Option<&'a str>,
}

impl<'a> FromReader<'a> for ComponentExternName<'a> {
Expand Down Expand Up @@ -142,7 +144,7 @@ impl<'a> FromReader<'a> for ComponentExternName<'a> {
0x01 => false,

0x02 => {
if reader.cm_implements() {
if reader.cm_implements() || reader.cm_canon_names() {
true
} else {
bail!(
Expand All @@ -157,6 +159,8 @@ impl<'a> FromReader<'a> for ComponentExternName<'a> {
let mut ret = ComponentExternName {
name: reader.read_string()?,
implements: None,
version_suffix: None,
external_id: None,
};
if has_options {
for _ in 0..reader.read_var_u32()? {
Expand All @@ -168,6 +172,18 @@ impl<'a> FromReader<'a> for ComponentExternName<'a> {
}
ret.implements = Some(name);
}
ComponentNameOpt::VersionSuffix(name) => {
if ret.version_suffix.is_some() {
bail!(pos, "duplicate 'versionsuffix' option in name");
}
ret.version_suffix = Some(name);
}
ComponentNameOpt::ExternalId(name) => {
if ret.external_id.is_some() {
bail!(pos, "duplicate 'external-id' option in name");
}
ret.external_id = Some(name);
}
}
}
}
Expand All @@ -177,12 +193,16 @@ impl<'a> FromReader<'a> for ComponentExternName<'a> {

enum ComponentNameOpt<'a> {
Implements(&'a str),
VersionSuffix(&'a str),
ExternalId(&'a str),
}

impl<'a> FromReader<'a> for ComponentNameOpt<'a> {
fn from_reader(reader: &mut BinaryReader<'a>) -> Result<Self> {
match reader.read_u8()? {
0x00 => Ok(ComponentNameOpt::Implements(reader.read()?)),
0x01 => Ok(ComponentNameOpt::VersionSuffix(reader.read()?)),
0x02 => Ok(ComponentNameOpt::ExternalId(reader.read()?)),
x => return reader.invalid_leading_byte(x, "name option"),
}
}
Expand Down
73 changes: 52 additions & 21 deletions crates/wasmparser/src/validator/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4654,15 +4654,17 @@ impl ComponentNameContext {
info: &mut TypeInfo,
features: &WasmFeatures,
) -> Result<()> {
let ComponentExternName {
name,
implements,
external_id,
version_suffix,
} = *name;
// First validate that `name` is even a valid kebab name, meaning it's
// in kebab-case, is an ID, etc.
let kebab =
ComponentName::new_with_features(name.name, offset, *features).with_context(|| {
format!(
"{} name `{}` is not a valid extern name",
kind.desc(),
name.name
)
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 {
Expand All @@ -4676,29 +4678,25 @@ impl ComponentNameContext {
ComponentNameKind::Hash(_)
| ComponentNameKind::Url(_)
| ComponentNameKind::Dependency(_) => {
bail!(offset, "name `{}` is not a valid export name", name.name)
bail!(offset, "name `{name}` is not a valid export name")
}
}
}

if let Some(implements) = name.implements {
if let Some(implements) = implements {
require_feature::cm_implements(
*features,
"the `cm-implements` feature is not active",
offset,
)?;
match kebab.kind() {
ComponentNameKind::Label(_) => {}
_ => bail!(
offset,
"name `{}` is not valid with `implements`",
name.name
),
_ => bail!(offset, "name `{name}` is not valid with `implements`",),
}

match ty {
ComponentEntityType::Instance(_) => {}
_ => bail!(offset, "only instance names can have an `implements`"),
_ => bail!(offset, "only instances can have an `implements`"),
}

let implements = ComponentName::new_with_features(implements, offset, *features)
Expand All @@ -4709,9 +4707,33 @@ impl ComponentNameContext {
}
}

if let Some(_) = version_suffix {
require_feature::cm_canon_names(
*features,
"the `cm-canon-names` feature is not active",
offset,
)?;
match ty {
ComponentEntityType::Instance(_) => {}
_ => bail!(offset, "only instances can have an `versionsuffix`"),
}
}

if let Some(_) = external_id {
require_feature::cm_implements(
*features,
"the `cm-implements` feature is not active",
offset,
)?;
match ty {
ComponentEntityType::Instance(_) => {}
_ => bail!(offset, "only instances can have an `external-id`"),
}
}

// Validate that the kebab name, if it has structure such as
// `[method]a.b`, is indeed valid with respect to known resources.
self.validate(&kebab, ty, types, offset)
self.validate(&kebab, version_suffix, ty, types, offset)
.with_context(|| format!("{} name `{kebab}` is not valid", kind.desc()))?;

// Top-level kebab-names must all be unique, even between both imports
Expand All @@ -4728,20 +4750,21 @@ impl ComponentNameContext {
// Otherwise all strings must be unique, regardless of their name, so
// consult the `items` set to ensure that we're not for example
// importing the same interface ID twice.
match items.entry(name.name.to_string()) {
match items.entry(name.to_string()) {
Entry::Occupied(e) => {
bail!(
offset,
"{kind} name `{name}` conflicts with previous name `{prev}`",
name = name.name,
kind = kind.desc(),
prev = e.key(),
);
}
Entry::Vacant(e) => {
e.insert(ComponentItem {
ty: *ty,
implements: name.implements.map(|s| s.to_string()),
implements: implements.map(|s| s.to_string()),
version_suffix: version_suffix.map(|s| s.to_string()),
external_id: external_id.map(|s| s.to_string()),
});
info.combine(ty.info(types), offset)?;
}
Expand All @@ -4753,6 +4776,7 @@ impl ComponentNameContext {
fn validate(
&self,
name: &ComponentName,
version_suffix: Option<&str>,
ty: &ComponentEntityType,
types: &TypeAlloc,
offset: usize,
Expand All @@ -4768,10 +4792,17 @@ impl ComponentNameContext {
match name.kind() {
// No validation necessary for these styles of names
ComponentNameKind::Label(_)
| ComponentNameKind::Interface(_)
| ComponentNameKind::Url(_)
| ComponentNameKind::Dependency(_)
| ComponentNameKind::Hash(_) => {}
| ComponentNameKind::Hash(_)
| ComponentNameKind::Dependency(_) => {}

// Validate the `version_suffix` field in the context of interface
// names.
ComponentNameKind::Interface(name) => {
if let Err(e) = name.version(version_suffix) {
bail!(offset, "invalid interface version: {e}");
}
}

// Constructors must return `(own $resource)` or
// `(result (own $Tresource))` and the `$resource` must be named
Expand Down
4 changes: 4 additions & 0 deletions crates/wasmparser/src/validator/component_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,10 @@ pub struct ComponentItem {
pub ty: ComponentEntityType,
/// The optional `(implements "...")` metadata, if specified.
pub implements: Option<String>,
/// The optional `(versionsuffix "...")` metadata, if specified.
pub version_suffix: Option<String>,
/// The optional `(external_id "...")` metadata, if specified.
pub external_id: Option<String>,
}

impl TypeData for ComponentType {
Expand Down
26 changes: 20 additions & 6 deletions crates/wasmparser/src/validator/names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,9 +524,19 @@ impl<'a> InterfaceName<'a> {
}

/// 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())
pub fn version(&self, suffix: Option<&str>) -> Result<Option<Version>, semver::Error> {
let Some(at) = self.0.find('@') else {
return Ok(None);
};
let prefix = &self.0[at + 1..];
match suffix {
// FIXME: this should perform full validation of the version
// suffix/prefix, notably that "prefix" is indeed the "semver track"
// that is expected. For example "1.2.3" means that prefix must be
// "1", nothing else. This validation is deferred to a future PR.
Some(suffix) => Ok(Some(Version::parse(&format!("{prefix}{suffix}"))?)),
None => Ok(Some(Version::parse(prefix)?)),
}
}
}

Expand Down Expand Up @@ -659,16 +669,20 @@ impl<'a> ComponentNameParser<'a> {
}

// pkgname ::= <pkgpath> <version>?
fn pkg_name(&mut self, require_projection: bool) -> Result<()> {
self.pkg_path(require_projection)?;
fn pkg_name(&mut self, is_interface_name: bool) -> Result<()> {
self.pkg_path(is_interface_name)?;

if self.eat_str("@") {
let version = match self.eat_up_to('>') {
Some(version) => version,
None => self.take_rest(),
};

self.semver(version)?;
// Validation of the semver of interface names is deferred to full
// component validation with access to the `version_suffix` field.
if !is_interface_name {
self.semver(version)?;
}
}

Ok(())
Expand Down
22 changes: 20 additions & 2 deletions crates/wasmprinter/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,13 +393,31 @@ impl Printer<'_, '_> {
}

fn print_component_extern_name(&mut self, name: &ComponentExternName<'_>) -> Result<()> {
self.print_str(name.name)?;
if let Some(implements) = name.implements {
let ComponentExternName {
name,
implements,
version_suffix,
external_id,
} = name;
self.print_str(name)?;
if let Some(implements) = implements {
self.result.write_str(" ")?;
self.start_group("implements ")?;
self.print_str(implements)?;
self.end_group()?;
}
if let Some(version_suffix) = version_suffix {
self.result.write_str(" ")?;
self.start_group("versionsuffix ")?;
self.print_str(version_suffix)?;
self.end_group()?;
}
if let Some(external_id) = external_id {
self.result.write_str(" ")?;
self.start_group("external-id ")?;
self.print_str(external_id)?;
self.end_group()?;
}
Ok(())
}

Expand Down
Loading
Loading