opa_wasm/
types.rs

1// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Type wrappers to help with interacting with the OPA WASM module
16
17use std::ffi::CStr;
18
19use anyhow::{bail, Context, Result};
20use serde::Deserialize;
21use wasmtime::{AsContext, AsContextMut, Instance, Memory};
22
23/// An entrypoint ID, as returned by the `entrypoints` export, and given to the
24/// `opa_eval_ctx_set_entrypoint` exports
25#[derive(Debug, Deserialize, Clone)]
26#[serde(transparent)]
27pub struct EntrypointId(pub(crate) i32);
28
29/// The ID of a builtin, as returned by the `builtins` export, and passed to the
30/// `opa_builtin*` imports
31#[derive(Debug, Deserialize, Clone)]
32#[serde(transparent)]
33pub struct BuiltinId(pub(crate) i32);
34
35/// A value stored on the WASM heap, as used by the `opa_value_*` exports
36#[derive(Debug)]
37pub struct Value(pub(crate) i32);
38
39/// A generic value on the WASM memory
40#[derive(Debug)]
41pub struct Addr(pub(crate) i32);
42
43/// A heap allocation on the WASM memory
44#[derive(Debug)]
45pub struct Heap {
46    /// The pointer to the start of the heap allocation
47    pub(crate) ptr: i32,
48
49    /// The length of the heap allocation
50    pub(crate) len: i32,
51
52    /// Whether the heap allocation has been freed
53    pub(crate) freed: bool,
54}
55
56impl Heap {
57    /// Get the end of the heap allocation
58    pub const fn end(&self) -> i32 {
59        self.ptr + self.len
60    }
61
62    /// Calculate the number of pages this heap allocation occupies
63    pub fn pages(&self) -> u64 {
64        let page_size = 64 * 1024;
65        let addr = self.end();
66        let page = addr / page_size;
67        // This is safe as the heap pointers will never be negative. We use i32 for
68        // convenience to avoid having to cast all the time.
69        #[allow(clippy::expect_used)]
70        if addr % page_size > 0 { page + 1 } else { page }
71            .try_into()
72            .expect("invalid heap address")
73    }
74}
75
76impl Drop for Heap {
77    fn drop(&mut self) {
78        if !self.freed {
79            tracing::warn!("forgot to free heap allocation");
80            self.freed = true;
81        }
82    }
83}
84
85/// A null-terminated string on the WASM memory
86#[derive(Debug)]
87pub struct NulStr(pub(crate) i32);
88
89impl NulStr {
90    /// Read the null-terminated string from the WASM memory
91    pub fn read<'s, T: AsContext>(&self, store: &'s T, memory: &Memory) -> Result<&'s CStr> {
92        let mem = memory.data(store);
93        let start: usize = self.0.try_into().context("invalid address")?;
94        let mem = mem.get(start..).context("memory address out of bounds")?;
95        let nul = mem
96            .iter()
97            .position(|c| *c == 0)
98            .context("malformed string")?;
99        let mem = mem
100            .get(..=nul)
101            .context("issue while extracting nul-terminated string")?;
102        let res = CStr::from_bytes_with_nul(mem)?;
103        Ok(res)
104    }
105}
106
107/// The address of the evaluation context, used by the `opa_eval_ctx_*` exports
108#[derive(Debug)]
109pub struct Ctx(pub(crate) i32);
110
111/// An error returned by the OPA module
112#[derive(Debug, thiserror::Error)]
113pub enum OpaError {
114    /// Unrecoverable internal error
115    #[error("Unrecoverable internal error")]
116    Internal,
117
118    /// Invalid value type was encountered
119    #[error("Invalid value type was encountered")]
120    InvalidType,
121
122    /// Invalid object path reference
123    #[error("Invalid object path reference")]
124    InvalidPath,
125
126    /// Unrecognized error code
127    #[error("Unrecognized error code: {0}")]
128    Other(i32),
129}
130
131impl OpaError {
132    /// Convert an error code to an `OpaError`
133    pub(crate) fn from_code(code: i32) -> Result<(), OpaError> {
134        match code {
135            0 => Ok(()),
136            1 => Err(Self::Internal),
137            2 => Err(Self::InvalidType),
138            3 => Err(Self::InvalidPath),
139            x => Err(Self::Other(x)),
140        }
141    }
142}
143
144/// Represents the ABI version of a WASM OPA module
145#[derive(Debug, Clone, Copy)]
146pub enum AbiVersion {
147    /// Version 1.0
148    V1_0,
149
150    /// Version 1.1
151    V1_1,
152
153    /// Version 1.2
154    V1_2,
155
156    /// Version >1.2, <2.0
157    V1_2Plus(i32),
158}
159
160impl AbiVersion {
161    /// Get the ABI version out of an instanciated WASM policy
162    ///
163    /// # Errors
164    ///
165    /// Returns an error if the WASM module lacks ABI version information
166    pub(crate) fn from_instance<T: Send>(
167        mut store: impl AsContextMut<Data = T>,
168        instance: &Instance,
169    ) -> Result<Self> {
170        let abi_version = instance
171            .get_global(&mut store, "opa_wasm_abi_version")
172            .context("missing global opa_wasm_abi_version")?
173            .get(&mut store)
174            .i32()
175            .context("opa_wasm_abi_version is not an i32")?;
176
177        let abi_minor_version = instance
178            .get_global(&mut store, "opa_wasm_abi_minor_version")
179            .context("missing global opa_wasm_abi_minor_version")?
180            .get(&mut store)
181            .i32()
182            .context("opa_wasm_abi_minor_version is not an i32")?;
183
184        Self::new(abi_version, abi_minor_version)
185    }
186
187    /// Create a new ABI version out of the minor and major version numbers.
188    fn new(major: i32, minor: i32) -> Result<Self> {
189        match (major, minor) {
190            (1, 0) => Ok(Self::V1_0),
191            (1, 1) => Ok(Self::V1_1),
192            (1, 2) => Ok(Self::V1_2),
193            (1, n @ 2..) => Ok(Self::V1_2Plus(n)),
194            (major, minor) => bail!("unsupported ABI version {}.{}", major, minor),
195        }
196    }
197
198    /// Check if this ABI version has support for the `eval` fastpath
199    #[must_use]
200    pub(crate) const fn has_eval_fastpath(self) -> bool {
201        matches!(self, Self::V1_2 | Self::V1_2Plus(_))
202    }
203}
204
205impl std::fmt::Display for AbiVersion {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        match self {
208            AbiVersion::V1_0 => write!(f, "1.0"),
209            AbiVersion::V1_1 => write!(f, "1.1"),
210            AbiVersion::V1_2 => write!(f, "1.2"),
211            AbiVersion::V1_2Plus(n) => write!(f, "1.{n} (1.2+ compatible)"),
212        }
213    }
214}