use trusty_common::memory_core::palace::PalaceId;
use trusty_common::memory_core::PalaceRegistry;
#[cfg_attr(not(test), allow(unused_imports))]
use trusty_common::uds::server::{
RpcError, CODE_INTERNAL_ERROR, CODE_INVALID_PARAMS, CODE_METHOD_NOT_FOUND,
};
use crate::AppState;
pub const CODE_NOT_FOUND: i64 = -32004;
pub const CODE_REFUSED: i64 = -32006;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
BadRequest,
NotFound,
Refused,
Internal,
}
impl ErrorKind {
fn code(self) -> i64 {
match self {
Self::BadRequest => CODE_INVALID_PARAMS,
Self::NotFound => CODE_NOT_FOUND,
Self::Refused => CODE_REFUSED,
Self::Internal => CODE_INTERNAL_ERROR,
}
}
}
#[derive(Debug, Clone)]
pub struct ApiError {
pub kind: ErrorKind,
pub message: String,
}
impl ApiError {
pub fn bad_request(msg: impl Into<String>) -> Self {
Self {
kind: ErrorKind::BadRequest,
message: msg.into(),
}
}
pub fn not_found(msg: impl Into<String>) -> Self {
Self {
kind: ErrorKind::NotFound,
message: msg.into(),
}
}
pub fn conflict(msg: impl Into<String>) -> Self {
Self {
kind: ErrorKind::Refused,
message: msg.into(),
}
}
pub fn forbidden(msg: impl Into<String>) -> Self {
Self {
kind: ErrorKind::Refused,
message: msg.into(),
}
}
pub fn unprocessable(msg: impl Into<String>) -> Self {
Self {
kind: ErrorKind::BadRequest,
message: msg.into(),
}
}
pub fn internal(msg: impl Into<String>) -> Self {
Self {
kind: ErrorKind::Internal,
message: msg.into(),
}
}
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for ApiError {}
impl From<ApiError> for RpcError {
fn from(e: ApiError) -> Self {
RpcError::new(e.kind.code(), e.message)
}
}
impl From<crate::service::ServiceError> for ApiError {
fn from(e: crate::service::ServiceError) -> Self {
match e {
crate::service::ServiceError::BadRequest(m) => ApiError::bad_request(m),
crate::service::ServiceError::NotFound(m) => ApiError::not_found(m),
crate::service::ServiceError::Conflict(m) => ApiError::conflict(m),
crate::service::ServiceError::Internal(m) => ApiError::internal(m),
crate::service::ServiceError::Forbidden(m) => ApiError::forbidden(m),
}
}
}
pub fn open_handle(
state: &AppState,
id: &str,
) -> Result<std::sync::Arc<trusty_common::memory_core::PalaceHandle>, ApiError> {
state
.registry
.open_palace(&state.data_root, &PalaceId::new(id))
.map_err(|e| {
if PalaceRegistry::open_error_is_absent(&e) {
ApiError::not_found(format!("palace not found: {id} ({e:#})"))
} else {
ApiError::internal(format!("palace could not be loaded: {id} ({e:#})"))
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn api_error_kind_maps_to_its_rpc_code() {
let cases = [
(ApiError::bad_request("x"), CODE_INVALID_PARAMS),
(ApiError::unprocessable("x"), CODE_INVALID_PARAMS),
(ApiError::not_found("x"), -32004),
(ApiError::conflict("x"), -32006),
(ApiError::forbidden("x"), -32006),
(ApiError::internal("x"), CODE_INTERNAL_ERROR),
];
for (error, expected) in cases {
let kind = error.kind;
let rpc: RpcError = error.into();
assert_eq!(rpc.code, expected, "{kind:?} must map to {expected}");
}
assert!(
!cases_map_to(CODE_METHOD_NOT_FOUND),
"no handler failure may impersonate method_not_found"
);
}
fn cases_map_to(code: i64) -> bool {
[
ErrorKind::BadRequest,
ErrorKind::NotFound,
ErrorKind::Refused,
ErrorKind::Internal,
]
.iter()
.any(|k| k.code() == code)
}
#[test]
fn api_error_message_survives_the_conversion() {
let rpc: RpcError = ApiError::not_found("palace not found: alpha").into();
assert_eq!(rpc.message, "palace not found: alpha");
}
#[tokio::test]
async fn unreadable_palace_is_internal_not_not_found_at_open_handle() {
use std::os::unix::fs::PermissionsExt as _;
if unsafe { libc::geteuid() } == 0 {
eprintln!("SKIP: running as root, so 0o000 does not deny this process");
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
let state = AppState::new(tmp.path().to_path_buf());
let dir = tmp.path().join("unreadable");
std::fs::create_dir_all(&dir).expect("create the palace directory");
std::fs::write(dir.join("palace.json"), "{}").expect("seed metadata");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o000))
.expect("deny access");
let failure = open_handle(&state, "unreadable")
.err()
.expect("an unreadable palace cannot be opened");
let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700));
assert_eq!(
failure.kind,
ErrorKind::Internal,
"a palace that is present but unreadable must not report as absent: {}",
failure.message
);
}
}