cli/
uri.rs

1// SPDX-License-Identifier: GPL-3-0-or-later
2// Copyright (c) 2025 Opinsys Oy
3
4use std::{fmt, num::ParseIntError, str::FromStr};
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8pub enum UriError {
9    #[error("unsupported URI scheme: '{0}'")]
10    UnsupportedScheme(String),
11    #[error("invalid handle format: {0}")]
12    ParseHandle(#[from] ParseIntError),
13    #[error("invalid context grip format: '{0}'")]
14    InvalidGripFormat(String),
15    #[error("I/O error: {0}")]
16    Io(#[from] std::io::Error),
17    #[error("operation not valid for this URI type")]
18    InvalidUriType,
19    #[error("invalid password format: {0}")]
20    InvalidPasswordFormat(String),
21}
22
23/// A type-safe representation of a resource identifier.
24#[derive(Debug, Clone, PartialEq)]
25pub enum Uri {
26    Tpm(u32),
27    Context(String),
28    Path(std::path::PathBuf),
29    Password(Vec<u8>),
30    Session(u32),
31}
32
33impl Uri {
34    /// Checks if the URI variant can be used as a parent object.
35    #[must_use]
36    pub fn is_parent(&self) -> bool {
37        matches!(self, Self::Tpm(_) | Self::Context(_) | Self::Path(_))
38    }
39
40    /// Reads the contents of a file path URI.
41    ///
42    /// # Errors
43    ///
44    /// Returns `UriError::Io` on read failure or `UriError::InvalidUriType`
45    /// if called on a non-Path variant.
46    pub fn to_bytes(&self) -> Result<Vec<u8>, UriError> {
47        match self {
48            Self::Path(path) => Ok(std::fs::read(path)?),
49            _ => Err(UriError::InvalidUriType),
50        }
51    }
52
53    /// Extracts the handle from a TPM or Session URI.
54    ///
55    /// # Errors
56    ///
57    /// Returns `UriError::InvalidUriType` if called on a non-handle variant.
58    pub fn to_handle(&self) -> Result<u32, UriError> {
59        match self {
60            Self::Tpm(handle) | Self::Session(handle) => Ok(*handle),
61            _ => Err(UriError::InvalidUriType),
62        }
63    }
64}
65
66impl FromStr for Uri {
67    type Err = UriError;
68
69    fn from_str(s: &str) -> Result<Self, Self::Err> {
70        if let Some(handle_str) = s.strip_prefix("tpm://") {
71            let handle = u32::from_str_radix(handle_str.trim_start_matches("0x"), 16)?;
72            Ok(Self::Tpm(handle))
73        } else if let Some(handle_str) = s.strip_prefix("session://") {
74            let handle = u32::from_str_radix(handle_str.trim_start_matches("0x"), 16)?;
75            Ok(Self::Session(handle))
76        } else if let Some(grip) = s.strip_prefix("key://") {
77            if grip.len() == 16 && grip.chars().all(|c| c.is_ascii_hexdigit()) {
78                Ok(Self::Context(grip.to_string()))
79            } else {
80                Err(UriError::InvalidGripFormat(grip.to_string()))
81            }
82        } else if let Some(hex_pass) = s.strip_prefix("password://") {
83            let bytes = hex::decode(hex_pass)
84                .map_err(|e| UriError::InvalidPasswordFormat(e.to_string()))?;
85            Ok(Self::Password(bytes))
86        } else if s.contains("://") {
87            Err(UriError::UnsupportedScheme(
88                s.split_once("://").unwrap_or(("", "")).0.to_string(),
89            ))
90        } else {
91            Ok(Self::Path(s.to_string().into()))
92        }
93    }
94}
95
96impl fmt::Display for Uri {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        match self {
99            Self::Tpm(handle) => write!(f, "tpm://{handle:08x}"),
100            Self::Context(grip) => write!(f, "key://{grip}"),
101            Self::Path(path) => write!(f, "{}", path.to_string_lossy()),
102            Self::Password(bytes) => write!(f, "password://{}", hex::encode(bytes)),
103            Self::Session(handle) => write!(f, "session://{handle:08x}"),
104        }
105    }
106}