blob: 417e5d3ccf1d6983c93bb55ce3ba28606930267a [file] [log] [blame] [edit]
// Copyright 2024 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! This module is concerned with comparing different platform versions. It
//! contains data structure to represent the platform API in ways that are
//! convenient for comparison, and the comparison algorithms.
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fmt::{Display, Write as _};
use anyhow::Result;
use flyweights::FlyStr;
use crate::ir::Openness;
use crate::{Configuration, Scope, Version};
mod types;
pub use types::*;
mod handle;
pub use handle::*;
mod path;
pub use path::*;
mod primitive;
pub use primitive::*;
mod problems;
pub use problems::*;
#[cfg(test)]
mod protocol_tests;
#[cfg(test)]
mod test;
#[cfg(test)]
mod types_tests;
#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Clone, Copy)]
pub enum Optionality {
Optional,
Required,
}
#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Clone, Copy)]
pub enum Flexibility {
Strict,
Flexible,
}
impl Display for Flexibility {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Flexibility::Strict => f.write_str("strict"),
Flexibility::Flexible => f.write_str("flexible"),
}
}
}
#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Clone, Copy)]
pub enum Transport {
Channel,
Driver,
}
impl Display for Transport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Transport::Channel => f.write_str("Channel"),
Transport::Driver => f.write_str("Driver"),
}
}
}
impl From<&Option<String>> for Transport {
fn from(value: &Option<String>) -> Self {
match value {
Some(transport) => transport.into(),
None => Self::Channel,
}
}
}
impl From<&String> for Transport {
fn from(value: &String) -> Self {
match value.as_str() {
"Channel" => Self::Channel,
"Driver" => Self::Driver,
_ => panic!("unknown protocol transport {value}"),
}
}
}
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub enum MethodPayload {
TwoWay(Type, Type),
OneWay(Type),
Event(Type),
}
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct Method {
pub name: FlyStr,
pub path: Path,
pub flexibility: Flexibility,
pub payload: MethodPayload,
}
impl Method {
pub fn two_way(
name: impl Into<FlyStr>,
path: Path,
flexibility: Flexibility,
request: Type,
response: Type,
) -> Self {
Self {
name: name.into(),
path,
flexibility,
payload: MethodPayload::TwoWay(request, response),
}
}
pub fn one_way(
name: impl Into<FlyStr>,
path: Path,
flexibility: Flexibility,
request: Type,
) -> Self {
Self { name: name.into(), path, flexibility, payload: MethodPayload::OneWay(request) }
}
pub fn event(
name: impl Into<FlyStr>,
path: Path,
flexibility: Flexibility,
payload: Type,
) -> Self {
Self { name: name.into(), path, flexibility, payload: MethodPayload::Event(payload) }
}
fn kind(&self) -> &'static str {
use MethodPayload::*;
match self.payload {
TwoWay(_, _) => "two-way",
OneWay(_) => "one-way",
Event(_) => "event",
}
}
fn server_initiated(&self) -> bool {
matches!(self.payload, MethodPayload::Event(_))
}
fn client_initiated(&self) -> bool {
!self.server_initiated()
}
fn is_one_way(&self) -> bool {
matches!(self.payload, MethodPayload::OneWay(_))
}
pub fn compatible(
client: &Self,
server: &Self,
config: &Configuration,
) -> CompatibilityProblems {
let mut problems = CompatibilityProblems::default();
if client.kind() != server.kind() {
problems.error(
[&client.path, &server.path],
format!(
"Interaction kind differs between client(@{}):{} and server(@{}):{}",
client.path.api_level(),
client.kind(),
server.path.api_level(),
server.kind()
),
);
return problems;
}
use MethodPayload::*;
match (&client.payload, &server.payload) {
(TwoWay(c_request, c_response), TwoWay(s_request, s_response)) => {
// Request
let compat = compare_types(c_request, s_request, &config);
if compat.is_incompatible() {
problems.error(
[&client.path, &server.path],
format!(
"Incompatible request types, client(@{}), server(@{})",
client.path.api_level(),
server.path.api_level(),
),
);
}
problems.append(compat);
// Response
let compat = compare_types(s_response, c_response, &config);
if compat.is_incompatible() {
problems.error(
[&client.path, &server.path],
format!(
"Incompatible response types, client(@{}), server(@{})",
client.path.api_level(),
server.path.api_level(),
),
);
}
problems.append(compat);
}
(OneWay(c_request), OneWay(s_request)) => {
let compat = compare_types(c_request, s_request, &config);
if compat.is_incompatible() {
problems.error(
[&client.path, &server.path],
format!(
"Incompatible request types, client(@{}), server(@{})",
client.path.api_level(),
server.path.api_level(),
),
);
}
problems.append(compat);
}
(Event(c_payload), Event(s_payload)) => {
let compat = compare_types(s_payload, c_payload, &config);
if compat.is_incompatible() {
problems.error(
[&client.path, &server.path],
format!(
"Incompatible event types, client(@{}), server(@{})",
client.path.api_level(),
server.path.api_level(),
),
);
}
problems.append(compat);
}
_ => (), // Interaction kind mismatch handled elsewhere.
}
problems
}
}
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
pub struct Scopes {
pub platform: bool,
pub external: bool,
}
impl Scopes {
pub fn contains(&self, scope: Scope) -> bool {
match scope {
Scope::Platform => self.platform,
Scope::External => self.external,
}
}
fn everywhere(&self) -> bool {
self.platform && self.external
}
}
impl std::fmt::Display for Scopes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Scopes { platform: false, external: false } => Ok(()),
Scopes { platform: true, external: false } => f.write_str("platform"),
Scopes { platform: false, external: true } => f.write_str("external"),
Scopes { platform: true, external: true } => f.write_str("platform,external"),
}
}
}
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
pub struct Discoverable {
pub name: String,
pub client: Scopes,
pub server: Scopes,
}
impl Discoverable {
fn render_for_message(&self) -> String {
let mut message = "@discoverable(".to_owned();
if !self.client.everywhere() {
write!(&mut message, "client=\"{}\"", self.client).unwrap();
}
if !self.client.everywhere() && !self.server.everywhere() {
message.push_str(", ");
}
if !self.server.everywhere() {
write!(&mut message, "server=\"{}\"", self.server).unwrap();
}
message.push(')');
message
}
}
#[test]
fn discoverable_render_for_message() {
assert_eq!(
"@discoverable()",
Discoverable {
name: "".to_owned(),
client: Scopes { platform: true, external: true },
server: Scopes { platform: true, external: true }
}
.render_for_message()
);
assert_eq!(
"@discoverable(client=\"external\")",
Discoverable {
name: "".to_owned(),
client: Scopes { platform: false, external: true },
server: Scopes { platform: true, external: true }
}
.render_for_message()
);
assert_eq!(
"@discoverable(server=\"platform\")",
Discoverable {
name: "".to_owned(),
client: Scopes { platform: true, external: true },
server: Scopes { platform: true, external: false }
}
.render_for_message()
);
assert_eq!(
"@discoverable(client=\"platform\", server=\"external\")",
Discoverable {
name: "".to_owned(),
client: Scopes { platform: true, external: false },
server: Scopes { platform: false, external: true }
}
.render_for_message()
);
}
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct Protocol {
pub name: FlyStr,
pub path: Path,
pub openness: Openness,
pub discoverable: Option<Discoverable>,
pub methods: BTreeMap<u64, Method>,
pub added: String,
}
impl Protocol {
fn compatible(protocols: [&Self; 2], config: &Configuration) -> CompatibilityProblems {
let mut problems = CompatibilityProblems::default();
// Look at discoverable to identify the pairs of client/server interactions
let mut clients = Vec::new();
let mut servers = Vec::new();
for p in protocols {
if let Some(d) = &p.discoverable {
let scope = p.path.scope();
if d.client.contains(scope) {
clients.push(p);
}
if d.server.contains(scope) {
servers.push(p);
}
}
}
for (client, server) in
clients.into_iter().flat_map(|c| servers.iter().map(move |s| (c, s)))
{
problems.append(Protocol::client_server_compatible(client, server, &config));
}
problems
}
fn client_server_compatible(
client: &Self,
server: &Self,
config: &Configuration,
) -> CompatibilityProblems {
let mut problems = CompatibilityProblems::default();
let client_ordinals: BTreeSet<u64> = client.methods.keys().cloned().collect();
let server_ordinals: BTreeSet<u64> = server.methods.keys().cloned().collect();
// Is there a contradiction between how this is declared and how it's used?
let mut scope_contradiction = false;
if let Some(disco) = &client.discoverable {
if !disco.client.contains(client.path.scope()) {
scope_contradiction = true;
problems.add(
!config.fatal_contradictions,
[&client.path, &server.path],
format!(
"Protocol {}(@{}) used as a {} client, contradicting its {}.",
client.name,
client.path.api_level(),
client.path.scope(),
disco.render_for_message()
),
);
}
}
if let Some(disco) = &server.discoverable {
if !disco.server.contains(server.path.scope()) {
scope_contradiction = true;
problems.add(
!config.fatal_contradictions,
[&client.path, &server.path],
format!(
"Protocol {}(@{}) used as a {} server, contradicting its {}.",
server.name,
server.path.api_level(),
server.path.scope(),
disco.render_for_message()
),
);
}
}
if scope_contradiction {
// If we've already found a contradiction, that means that:
// (a) this is discoverable so we'll be checking it elsewhere
// (b) any more checks we do are kind of moot.
return problems;
}
// Compare common interactions
for ordinal in client_ordinals.intersection(&server_ordinals) {
problems.append(Method::compatible(
client.methods.get(ordinal).expect("Unexpectedly missing client method"),
server.methods.get(ordinal).expect("Unexpectedly missing server method"),
&config,
));
}
// Only on client.
for ordinal in client_ordinals.difference(&server_ordinals) {
let method = client.methods.get(ordinal).expect("Unexpectedly missing client method");
if method.client_initiated() {
let client_sets_unknown_bit = method.flexibility == Flexibility::Flexible;
let server_allows_unknown = server.openness == Openness::Open
|| method.is_one_way() && server.openness == Openness::Ajar;
if client_sets_unknown_bit && server_allows_unknown {
// The server doesn't know about this method but: (a) the
// client sets the "unknown" header bit indicating that it's
// fine with the server not recognizing it, and (b) the
// server is expecting messages with the "unknown" bit set.
} else {
problems.error(
[&method.path, &server.path],
format!(
"Server(@{}) missing method {}.{}",
server.path.api_level(),
client.name,
method.name
),
);
}
}
}
// Only on server.
for ordinal in server_ordinals.difference(&client_ordinals) {
let method = server.methods.get(ordinal).expect("Unexpectedly missing server method");
if method.server_initiated() {
if method.flexibility == Flexibility::Flexible
&& (server.openness == Openness::Open || server.openness == Openness::Ajar)
{
// The server thinks the event is flexible and the client thinks the protocol is open enough.
} else {
problems.error(
[&client.path, &method.path],
format!(
"Client(@{}) missing event {}.{}",
client.path.api_level(),
server.name,
method.name
),
);
}
}
}
problems
}
}
#[derive(Clone, Debug)]
pub struct AbiSurface {
#[allow(unused)]
pub version: Version,
pub discoverable: HashMap<String, Protocol>,
pub tear_off: HashMap<String, Protocol>,
}
pub fn compatible(
surfaces: [&AbiSurface; 2],
config: &Configuration,
) -> Result<CompatibilityProblems> {
let mut problems = CompatibilityProblems::default();
// Discoverable protocols for each ABI surface.
let disco: [HashSet<String>; 2] = surfaces.map(|s| s.discoverable.keys().cloned().collect());
// Discoverable protocols shared between both ABI surfaces.
let common_disco: HashSet<String> = disco[0].intersection(&disco[1]).cloned().collect();
for p in common_disco {
problems.append(Protocol::compatible(surfaces.map(|s| &s.discoverable[&p]), &config));
}
problems.sort();
Ok(problems)
}