wasmer_backend_api/
global_id.rs

1//! [`GlobalId`]s are used by the backend to identify a specific object.
2//!
3//! This module provides a parser/encoder and related type defintions
4//! for global ids.
5
6use std::fmt::Display;
7
8#[repr(u16)]
9#[non_exhaustive]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum NodeKind {
12    User = 0,
13    SocialAuth = 1,
14    Namespace = 2,
15    Package = 3,
16    PackageVersion = 4,
17    PackageCollaborator = 5,
18    PackageCollaboratorInvite = 6,
19    NativeExecutable = 7,
20    PackageVersionNPMBinding = 8,
21    PackageVersionPythonBinding = 9,
22    PackageTransferRequest = 10,
23    Interface = 11,
24    InterfaceVersion = 12,
25    PublicKey = 13,
26    UserNotification = 14,
27    ActivityEvent = 15,
28    NamespaceCollaborator = 16,
29    NamespaceCollaboratorInvite = 17,
30    BindingsGenerator = 18,
31    DeployConfigVersion = 19,
32    DeployConfigInfo = 20,
33    DeployApp = 21,
34    DeployAppVersion = 22,
35    Waitlist = 23,
36    WaitlistMember = 24,
37    CardPaymentMethod = 25,
38    PaymentIntent = 26,
39    AppAlias = 27,
40    Nonce = 28,
41    TermsOfService = 29,
42}
43
44impl NodeKind {
45    pub fn from_num(x: u64) -> Option<Self> {
46        match x {
47            0 => Some(Self::User),
48            1 => Some(Self::SocialAuth),
49            2 => Some(Self::Namespace),
50            3 => Some(Self::Package),
51            4 => Some(Self::PackageVersion),
52            5 => Some(Self::PackageCollaborator),
53            6 => Some(Self::PackageCollaboratorInvite),
54            7 => Some(Self::NativeExecutable),
55            8 => Some(Self::PackageVersionNPMBinding),
56            9 => Some(Self::PackageVersionPythonBinding),
57            10 => Some(Self::PackageTransferRequest),
58            11 => Some(Self::Interface),
59            12 => Some(Self::InterfaceVersion),
60            13 => Some(Self::PublicKey),
61            14 => Some(Self::UserNotification),
62            15 => Some(Self::ActivityEvent),
63            16 => Some(Self::NamespaceCollaborator),
64            17 => Some(Self::NamespaceCollaboratorInvite),
65            18 => Some(Self::BindingsGenerator),
66            19 => Some(Self::DeployConfigVersion),
67            20 => Some(Self::DeployConfigInfo),
68            21 => Some(Self::DeployApp),
69            22 => Some(Self::DeployAppVersion),
70            23 => Some(Self::Waitlist),
71            24 => Some(Self::WaitlistMember),
72            25 => Some(Self::CardPaymentMethod),
73            26 => Some(Self::PaymentIntent),
74            27 => Some(Self::AppAlias),
75            28 => Some(Self::Nonce),
76            29 => Some(Self::TermsOfService),
77            _ => None,
78        }
79    }
80
81    pub fn parse_prefix(s: &str) -> Option<NodeKind> {
82        match s {
83            "u" => Some(Self::User),
84            "su" => Some(Self::SocialAuth),
85            "ns" => Some(Self::Namespace),
86            "pk" => Some(Self::Package),
87            "pkv" => Some(Self::PackageVersion),
88            "pc" => Some(Self::PackageCollaborator),
89            "pci" => Some(Self::PackageCollaboratorInvite),
90            "ne" => Some(Self::NativeExecutable),
91            "pkvbjs" => Some(Self::PackageVersionNPMBinding),
92            "pkvbpy" => Some(Self::PackageVersionPythonBinding),
93            "pt" => Some(Self::PackageTransferRequest),
94            "in" => Some(Self::Interface),
95            "inv" => Some(Self::InterfaceVersion),
96            "pub" => Some(Self::PublicKey),
97            "nt" => Some(Self::UserNotification),
98            "ae" => Some(Self::ActivityEvent),
99            "nsc" => Some(Self::NamespaceCollaborator),
100            "nsci" => Some(Self::NamespaceCollaboratorInvite),
101            "bg" => Some(Self::BindingsGenerator),
102            "dcv" => Some(Self::DeployConfigVersion),
103            "dci" => Some(Self::DeployConfigInfo),
104            "da" => Some(Self::DeployApp),
105            "dav" => Some(Self::DeployAppVersion),
106            "wl" => Some(Self::Waitlist),
107            "wlm" => Some(Self::WaitlistMember),
108            "cpm" => Some(Self::CardPaymentMethod),
109            "pi" => Some(Self::PaymentIntent),
110            "daa" => Some(Self::AppAlias),
111            "nnc" => Some(Self::Nonce),
112            "tos" => Some(Self::TermsOfService),
113            _ => None,
114        }
115    }
116
117    fn as_prefix(&self) -> &'static str {
118        match self {
119            Self::User => "u",
120            Self::SocialAuth => "su",
121            Self::Namespace => "ns",
122            Self::Package => "pk",
123            Self::PackageVersion => "pkv",
124            Self::PackageCollaborator => "pc",
125            Self::PackageCollaboratorInvite => "pci",
126            Self::NativeExecutable => "ne",
127            Self::PackageVersionNPMBinding => "pkvbjs",
128            Self::PackageVersionPythonBinding => "pkvbpy",
129            Self::PackageTransferRequest => "pt",
130            Self::Interface => "in",
131            Self::InterfaceVersion => "inv",
132            Self::PublicKey => "pub",
133            Self::UserNotification => "nt",
134            Self::ActivityEvent => "ae",
135            Self::NamespaceCollaborator => "nsc",
136            Self::NamespaceCollaboratorInvite => "nsci",
137            Self::BindingsGenerator => "bg",
138            Self::DeployConfigVersion => "dcv",
139            Self::DeployConfigInfo => "dci",
140            Self::DeployApp => "da",
141            Self::DeployAppVersion => "dav",
142            Self::Waitlist => "wl",
143            Self::WaitlistMember => "wlm",
144            Self::CardPaymentMethod => "cpm",
145            Self::PaymentIntent => "pi",
146            Self::AppAlias => "daa",
147            Self::Nonce => "nnc",
148            Self::TermsOfService => "tos",
149        }
150    }
151}
152
153impl Display for NodeKind {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        let name = match self {
156            Self::User => "User",
157            Self::SocialAuth => "SocialAuth",
158            Self::Namespace => "Namespace",
159            Self::Package => "Package",
160            Self::PackageVersion => "PackageVersion",
161            Self::PackageCollaborator => "PackageCollaborator",
162            Self::PackageCollaboratorInvite => "PackageCollaboratorInvite",
163            Self::NativeExecutable => "NativeExecutable",
164            Self::PackageVersionNPMBinding => "PackageVersionNPMBinding",
165            Self::PackageVersionPythonBinding => "PackageVersionPythonBinding",
166            Self::PackageTransferRequest => "PackageTransferRequest",
167            Self::Interface => "Interface",
168            Self::InterfaceVersion => "InterfaceVersion",
169            Self::PublicKey => "PublicKey",
170            Self::UserNotification => "UserNotification",
171            Self::ActivityEvent => "ActivityEvent",
172            Self::NamespaceCollaborator => "NamespaceCollaborator",
173            Self::NamespaceCollaboratorInvite => "NamespaceCollaboratorInvite",
174            Self::BindingsGenerator => "BindingsGenerator",
175            Self::DeployConfigVersion => "DeployConfigVersion",
176            Self::DeployConfigInfo => "DeployConfigInfo",
177            Self::DeployApp => "DeployApp",
178            Self::DeployAppVersion => "DeployAppVersion",
179            Self::Waitlist => "Waitlist",
180            Self::WaitlistMember => "WaitlistMember",
181            Self::CardPaymentMethod => "CardPaymentMethod",
182            Self::PaymentIntent => "PaymentIntent",
183            Self::AppAlias => "AppAlias",
184            Self::Nonce => "Nonce",
185            Self::TermsOfService => "TermsOfService",
186        };
187        write!(f, "{name}")
188    }
189}
190
191/// Global id of backend nodes.
192///
193/// IDs are encoded using the "hashid" scheme, which uses a given alphabet and
194/// a salt to encode u64 numbers into a string hash.
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
196pub struct GlobalId {
197    /// The node type of the ID.
198    kind: NodeKind,
199    /// The database ID of the node.
200    database_id: u64,
201}
202
203impl GlobalId {
204    /// Salt used by the backend to encode hashes.
205    const SALT: &'static str = "wasmer salt hashid";
206    /// Minimum length of the encoded hashes.
207    const MIN_LENGTH: usize = 12;
208
209    /// Hash alphabet used for the prefix id variant.
210    const ALPHABET_PREFIXED: &'static str =
211        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
212
213    /// Hash alphabet used for the non-prefixed id variant.
214    const ALPHABET_URL: &'static str = "abcdefghijklmnopqrstuvwxyz0123456789";
215
216    pub fn new(kind: NodeKind, database_id: u64) -> Self {
217        Self { kind, database_id }
218    }
219
220    fn build_harsh(alphabet: &str, salt: &[u8]) -> harsh::Harsh {
221        harsh::HarshBuilder::new()
222            .alphabet(alphabet.as_bytes())
223            .salt(salt)
224            .length(GlobalId::MIN_LENGTH)
225            .build()
226            .unwrap()
227    }
228
229    fn build_harsh_prefixed() -> harsh::Harsh {
230        Self::build_harsh(Self::ALPHABET_PREFIXED, Self::SALT.as_bytes())
231    }
232
233    fn build_harsh_url() -> harsh::Harsh {
234        Self::build_harsh(Self::ALPHABET_URL, Self::SALT.as_bytes())
235    }
236
237    pub fn kind(&self) -> NodeKind {
238        self.kind
239    }
240
241    pub fn database_id(&self) -> u64 {
242        self.database_id
243    }
244
245    /// Encode a prefixed global id.
246    pub fn encode_prefixed(&self) -> String {
247        let hash = Self::build_harsh_prefixed().encode(&[
248            // scope
249            1,
250            // version
251            2,
252            self.kind as u64,
253            self.database_id,
254        ]);
255
256        format!("{}_{}", self.kind.as_prefix(), hash)
257    }
258
259    fn parse_values(values: &[u64]) -> Result<Self, ErrorKind> {
260        let scope = values.first().cloned().ok_or(ErrorKind::MissingScope)?;
261
262        if scope != 1 {
263            return Err(ErrorKind::UnknownScope(scope));
264        }
265
266        let version = values.get(1).cloned().ok_or(ErrorKind::MissingVersion)?;
267        if version != 2 {
268            return Err(ErrorKind::UnknownVersion(version));
269        }
270
271        let ty_raw = values.get(2).cloned().ok_or(ErrorKind::MissingNodeType)?;
272        let ty_parsed = NodeKind::from_num(ty_raw).ok_or(ErrorKind::UnknownNodeType(ty_raw))?;
273
274        let db_id = values.get(3).cloned().ok_or(ErrorKind::MissingDatabaseId)?;
275
276        Ok(Self {
277            kind: ty_parsed,
278            database_id: db_id,
279        })
280    }
281
282    /// Parse a prefixed global id.
283    pub fn parse_prefixed(hash: &str) -> Result<Self, GlobalIdParseError> {
284        let (prefix, value) = hash
285            .split_once('_')
286            .ok_or_else(|| GlobalIdParseError::new(hash, ErrorKind::MissingPrefix))?;
287
288        if prefix.is_empty() {
289            return Err(GlobalIdParseError::new(hash, ErrorKind::MissingPrefix));
290        }
291
292        let ty_prefix = NodeKind::parse_prefix(prefix).ok_or_else(|| {
293            GlobalIdParseError::new(hash, ErrorKind::UnknownPrefix(prefix.to_string()))
294        })?;
295
296        let values = Self::build_harsh_prefixed()
297            .decode(value)
298            .map_err(|err| GlobalIdParseError::new(hash, ErrorKind::Decode(err.to_string())))?;
299
300        let s = Self::parse_values(&values).map_err(|kind| GlobalIdParseError::new(hash, kind))?;
301
302        if ty_prefix != s.kind {
303            return Err(GlobalIdParseError::new(hash, ErrorKind::PrefixTypeMismatch));
304        }
305
306        Ok(s)
307    }
308
309    /// Encode a non-prefixed global id.
310    ///
311    /// Note: URL ids use a different alphabet than prefixed ids.
312    pub fn encode_url(&self) -> String {
313        Self::build_harsh_url().encode(&[
314            // scope
315            1,
316            // version
317            2,
318            self.kind as u64,
319            self.database_id,
320        ])
321    }
322
323    /// Parse a non-prefixed URL global id variant.
324    ///
325    /// Note: URL ids use a different alphabet than prefixed ids.
326    pub fn parse_url(hash: &str) -> Result<Self, GlobalIdParseError> {
327        let values = Self::build_harsh_url()
328            .decode(hash)
329            .map_err(|err| GlobalIdParseError::new(hash, ErrorKind::Decode(err.to_string())))?;
330
331        Self::parse_values(&values).map_err(|kind| GlobalIdParseError::new(hash, kind))
332    }
333}
334
335#[derive(Debug, Clone, PartialEq, Eq)]
336pub struct GlobalIdParseError {
337    id: String,
338    kind: ErrorKind,
339}
340
341impl GlobalIdParseError {
342    fn new(id: impl Into<String>, kind: ErrorKind) -> Self {
343        Self {
344            id: id.into(),
345            kind,
346        }
347    }
348}
349
350/// Error type for parsing of [`GlobalId`]s.
351// Note: kept private on purpose, not useful to export.
352#[derive(Debug, Clone, PartialEq, Eq)]
353#[non_exhaustive]
354enum ErrorKind {
355    MissingPrefix,
356    UnknownPrefix(String),
357    PrefixTypeMismatch,
358    MissingScope,
359    UnknownScope(u64),
360    MissingVersion,
361    UnknownVersion(u64),
362    MissingNodeType,
363    UnknownNodeType(u64),
364    MissingDatabaseId,
365    Decode(String),
366}
367
368impl std::fmt::Display for GlobalIdParseError {
369    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
370        write!(f, "could not parse global id '{}': ", self.id)?;
371
372        match &self.kind {
373            ErrorKind::UnknownPrefix(p) => {
374                write!(f, "unknown type prefix '{p}'")
375            }
376            ErrorKind::Decode(s) => {
377                write!(f, "decode error: {s}")
378            }
379            ErrorKind::MissingScope => {
380                write!(f, "missing scope value")
381            }
382            ErrorKind::UnknownScope(x) => {
383                write!(f, "unknown scope value {x}")
384            }
385            ErrorKind::MissingVersion => {
386                write!(f, "missing version value")
387            }
388            ErrorKind::UnknownVersion(v) => {
389                write!(f, "unknown version value {v}")
390            }
391            ErrorKind::UnknownNodeType(t) => {
392                write!(f, "unknown node type '{t}'")
393            }
394            ErrorKind::MissingPrefix => write!(f, "missing prefix"),
395            ErrorKind::PrefixTypeMismatch => write!(f, "prefix type mismatch"),
396            ErrorKind::MissingNodeType => write!(f, "missing node type"),
397            ErrorKind::MissingDatabaseId => write!(f, "missing database id"),
398        }
399    }
400}
401
402impl std::error::Error for GlobalIdParseError {}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    #[test]
409    fn test_global_id() {
410        // Roundtrip.
411        let x1 = GlobalId {
412            kind: NodeKind::DeployApp,
413            database_id: 123,
414        };
415        assert_eq!(Ok(x1), GlobalId::parse_prefixed(&x1.encode_prefixed()));
416        assert_eq!(Ok(x1), GlobalId::parse_url(&x1.encode_url()));
417
418        assert_eq!(
419            GlobalId::parse_prefixed("da_MRrWI0t5U582"),
420            Ok(GlobalId {
421                kind: NodeKind::DeployApp,
422                database_id: 273,
423            })
424        );
425
426        // Error conditions.
427        assert_eq!(
428            GlobalId::parse_prefixed("oOtQIDI7q").err().unwrap().kind,
429            ErrorKind::MissingPrefix,
430        );
431        assert_eq!(
432            GlobalId::parse_prefixed("oOtQIDI7q").err().unwrap().kind,
433            ErrorKind::MissingPrefix,
434        );
435        assert_eq!(
436            GlobalId::parse_prefixed("_oOtQIDI7q").err().unwrap().kind,
437            ErrorKind::MissingPrefix,
438        );
439        assert_eq!(
440            GlobalId::parse_prefixed("lala_oOtQIDI7q")
441                .err()
442                .unwrap()
443                .kind,
444            ErrorKind::UnknownPrefix("lala".to_string()),
445        );
446
447        let kind = GlobalId::parse_prefixed("da_xxx").err().unwrap().kind;
448        assert!(matches!(kind, ErrorKind::Decode(_)));
449    }
450
451    #[test]
452    fn test_global_id_parse_values() {
453        assert_eq!(GlobalId::parse_values(&[]), Err(ErrorKind::MissingScope));
454        assert_eq!(
455            GlobalId::parse_values(&[2]),
456            Err(ErrorKind::UnknownScope(2)),
457        );
458        assert_eq!(GlobalId::parse_values(&[1]), Err(ErrorKind::MissingVersion));
459        assert_eq!(
460            GlobalId::parse_values(&[1, 999]),
461            Err(ErrorKind::UnknownVersion(999)),
462        );
463        assert_eq!(
464            GlobalId::parse_values(&[1, 2]),
465            Err(ErrorKind::MissingNodeType),
466        );
467        assert_eq!(
468            GlobalId::parse_values(&[1, 2, 99999]),
469            Err(ErrorKind::UnknownNodeType(99999)),
470        );
471        assert_eq!(
472            GlobalId::parse_values(&[1, 2, 1]),
473            Err(ErrorKind::MissingDatabaseId),
474        );
475        assert_eq!(
476            GlobalId::parse_values(&[1, 2, 1, 1]),
477            Ok(GlobalId {
478                kind: NodeKind::SocialAuth,
479                database_id: 1,
480            }),
481        );
482    }
483}