Refactor session ID handling and improve error management
- Introduced `SessionId` newtype for validated session identifiers, ensuring safety against path traversal attacks. - Updated session repository methods to accept `SessionId` instead of raw strings, enhancing type safety. - Removed redundant error handling in repository methods by leveraging the new `Error` type from `zesdex_utils`. - Simplified atomic JSON write operations by eliminating unnecessary error conversions. - Enhanced integer casting with a new `CastOr` trait for safer narrowing conversions. - Removed deprecated error handling code and consolidated error types across the codebase. - Updated HTTP handlers to utilize the new session ID validation, improving overall robustness.
This commit is contained in:
@@ -10,6 +10,8 @@ use std::path::Path;
|
||||
use serde::Serialize;
|
||||
use tracing;
|
||||
|
||||
use crate::Result;
|
||||
|
||||
/// Atomically write serializable `data` to `path`.
|
||||
///
|
||||
/// Flow: serialize to pretty JSON -> write to `path.tmp` -> fsync -> rename -> fsync parent.
|
||||
@@ -18,28 +20,33 @@ use tracing;
|
||||
/// Edge case: tmp file name uses `with_extension("tmp")` which replaces
|
||||
/// the existing extension -- correct for `foo.json` -> `foo.tmp`. For paths
|
||||
/// without an extension (unlikely in this codebase), appends `.tmp`.
|
||||
pub fn write_json_atomic<T: Serialize>(path: &Path, data: &T, mode: Option<u32>) -> anyhow::Result<()> {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `Error::Io` on I/O failures, `Error::Serde` on serialisation
|
||||
/// failures.
|
||||
pub fn write_json_atomic<T: Serialize>(path: &Path, data: &T, mode: Option<u32>) -> Result<()> {
|
||||
let tmp = path.with_extension("tmp"); // temporary sibling for atomic rename
|
||||
let bytes = serde_json::to_vec_pretty(data)?; // pretty-printed JSON
|
||||
let bytes = serde_json::to_vec_pretty(data)?; // pretty-printed JSON -> Error::Serde
|
||||
{
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(&tmp)?;
|
||||
f.write_all(&bytes)?;
|
||||
f.sync_all()?; // flush kernel buffers to disk
|
||||
.open(&tmp)?; // -> Error::Io
|
||||
f.write_all(&bytes)?; // -> Error::Io
|
||||
f.sync_all()?; // flush kernel buffers to disk -> Error::Io
|
||||
}
|
||||
if let Some(m) = mode {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(m))?;
|
||||
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(m))?; // -> Error::Io
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{ let _ = m; }
|
||||
}
|
||||
std::fs::rename(&tmp, path)?; // atomic move (POSIX guarantees it is atomic within the same fs)
|
||||
std::fs::rename(&tmp, path)?; // -> Error::Io (atomic move within same fs)
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::File::open(parent).and_then(|d| d.sync_all());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
//! Safe integer cast extension trait.
|
||||
//!
|
||||
//! Provides a `cast_or(self, default: U)` method on integer types that
|
||||
//! uses `TryFrom` for a checked narrowing conversion, falling back to a
|
||||
//! caller-supplied default on overflow. Replaces the `as` casts that
|
||||
//! used `#![allow(clippy::cast_*)]` across the codebase.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use zesdex_utils::CastOr;
|
||||
//!
|
||||
//! let len: usize = 42;
|
||||
//! let n: i64 = len.cast_or(-1); // Ok(42)
|
||||
//! ```
|
||||
|
||||
/// Extension trait for checked integer narrowing with a fallback default.
|
||||
///
|
||||
/// Implementations are provided for all commonly-used integer conversions
|
||||
/// via a macro. Each implementation calls `U::try_from(self).unwrap_or(default)`.
|
||||
pub trait CastOr<U> {
|
||||
/// Convert `self` to type `U`, returning `default` if the value overflows.
|
||||
fn cast_or(self, default: U) -> U;
|
||||
}
|
||||
|
||||
macro_rules! impl_cast_or {
|
||||
($from:ty => $($to:ty),+ $(,)?) => {
|
||||
$(
|
||||
impl CastOr<$to> for $from {
|
||||
#[inline]
|
||||
fn cast_or(self, default: $to) -> $to {
|
||||
<$to as TryFrom<$from>>::try_from(self).unwrap_or(default)
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
// usize → narrower types (same-archive-size signed version too)
|
||||
impl_cast_or!(usize => u64, i64, u32, i32, u16);
|
||||
|
||||
// u64 → narrower types
|
||||
impl_cast_or!(u64 => i64, u32, i32, u16, u8);
|
||||
|
||||
// i64 → narrower types
|
||||
impl_cast_or!(i64 => u64, i32, u16, u8);
|
||||
|
||||
// u32 → narrower types
|
||||
impl_cast_or!(u32 => i32, u16, u8);
|
||||
|
||||
// u128 → u64 (common for Duration math)
|
||||
impl CastOr<u64> for u128 {
|
||||
#[inline]
|
||||
fn cast_or(self, default: u64) -> u64 {
|
||||
u64::try_from(self).unwrap_or(default)
|
||||
}
|
||||
}
|
||||
|
||||
impl CastOr<i64> for u128 {
|
||||
#[inline]
|
||||
fn cast_or(self, default: i64) -> i64 {
|
||||
i64::try_from(self).unwrap_or(default)
|
||||
}
|
||||
}
|
||||
|
||||
impl CastOr<u32> for u128 {
|
||||
#[inline]
|
||||
fn cast_or(self, default: u32) -> u32 {
|
||||
u32::try_from(self).unwrap_or(default)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_usize_to_u64() {
|
||||
let v: usize = 100;
|
||||
assert_eq!(v.cast_or(0u64), 100u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_usize_to_i64() {
|
||||
let v: usize = 100;
|
||||
assert_eq!(v.cast_or(0i64), 100i64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_usize_to_u16_overflow() {
|
||||
let v: usize = 70000; // > u16::MAX
|
||||
assert_eq!(v.cast_or(42u16), 42u16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_u128_to_u64_overflow() {
|
||||
let v: u128 = u64::MAX as u128 + 1;
|
||||
assert_eq!(v.cast_or(999u64), 999u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_u64_to_i32_overflow() {
|
||||
let v: u64 = i32::MAX as u64 + 1;
|
||||
assert_eq!(v.cast_or(-1i32), -1i32);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +1,47 @@
|
||||
//! Shared error types for the zesdex codebase.
|
||||
//!
|
||||
//! Defines [`Error`], a unified error enum covering I/O, JSON, parse,
|
||||
//! not-found, and invalid-input cases, plus a [`Result`] type alias.
|
||||
//! Conversions from `std::io::Error` and `serde_json::Error` are provided
|
||||
//! via `From` impls.
|
||||
|
||||
use std::fmt;
|
||||
//! not-found, conflict, invalid-id, and invalid-input cases, plus a
|
||||
//! [`Result`] type alias. Conversions from `std::io::Error` and
|
||||
//! `serde_json::Error` are provided via `From` impls.
|
||||
//!
|
||||
//! This type is used directly by repository traits across crates,
|
||||
//! replacing per-crate `RepositoryError` duplications.
|
||||
|
||||
/// Unified error type for the zesdex codebase.
|
||||
#[derive(Debug)]
|
||||
///
|
||||
/// Serves as the shared `RepositoryError` for all persistence layers.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Wraps an I/O error.
|
||||
Io(std::io::Error),
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
/// Wraps a JSON serialization/deserialization error.
|
||||
Serde(serde_json::Error),
|
||||
#[error("serialization error: {0}")]
|
||||
Serde(#[from] serde_json::Error),
|
||||
/// A generic parse failure with a message.
|
||||
#[error("parse error: {0}")]
|
||||
Parse(String),
|
||||
/// A resource was not found.
|
||||
#[error("not found: {0}")]
|
||||
NotFound(String),
|
||||
/// A conflict occurred (e.g. duplicate entry).
|
||||
#[error("conflict: {0}")]
|
||||
Conflict(String),
|
||||
/// Invalid input was provided.
|
||||
#[error("invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
/// The supplied identifier is invalid (e.g. path traversal attempt).
|
||||
#[error("invalid id: {0}")]
|
||||
InvalidId(String),
|
||||
/// The session is locked and cannot be accessed.
|
||||
#[error("session is locked")]
|
||||
SessionLocked,
|
||||
/// An error that could not be cast to a specific variant.
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Io(e) => write!(f, "I/O error: {e}"),
|
||||
Self::Serde(e) => write!(f, "serialization error: {e}"),
|
||||
Self::Parse(msg) => write!(f, "parse error: {msg}"),
|
||||
Self::NotFound(resource) => write!(f, "not found: {resource}"),
|
||||
Self::InvalidInput(msg) => write!(f, "invalid input: {msg}"),
|
||||
Self::SessionLocked => write!(f, "session is locked"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Io(e) => Some(e),
|
||||
Self::Serde(e) => Some(e),
|
||||
Self::Parse(_) | Self::NotFound(_) | Self::InvalidInput(_) | Self::SessionLocked => {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// From conversions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Self::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Self::Serde(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Note: anyhow already provides `From<E> for anyhow::Error` for all
|
||||
// `E: std::error::Error + Send + Sync + 'static`, which our `Error` satisfies.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type alias
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -76,7 +50,7 @@ impl From<serde_json::Error> for Error {
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Additional impls
|
||||
// Constructors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
impl Error {
|
||||
@@ -94,4 +68,13 @@ impl Error {
|
||||
pub fn invalid_input(msg: impl Into<String>) -> Self {
|
||||
Self::InvalidInput(msg.into())
|
||||
}
|
||||
|
||||
/// Convert an `anyhow::Error` to `zesdex_utils::Error` by attempting
|
||||
/// downcast to known inner types.
|
||||
pub fn from_anyhow(e: anyhow::Error) -> Self {
|
||||
if let Some(ioe) = e.downcast_ref::<std::io::Error>() {
|
||||
return Error::Io(std::io::Error::new(ioe.kind(), ioe.to_string()));
|
||||
}
|
||||
Error::Other(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
pub mod atomic_write;
|
||||
pub use atomic_write::write_json_atomic;
|
||||
pub mod cast;
|
||||
pub use cast::CastOr;
|
||||
pub mod clipboard;
|
||||
pub mod error;
|
||||
pub mod logger;
|
||||
|
||||
Reference in New Issue
Block a user