tideorm 0.9.14

A developer-friendly ORM for Rust with clean, expressive syntax
Documentation
use serde::{Serialize, de::DeserializeOwned};

use crate::error::{Error, Result};

use super::{ModelMeta, UpdateValue};

pub(crate) fn encrypt_model_field<T>(
    value: T,
    table_name: &str,
    field_name: &str,
    column_name: &str,
) -> Result<T>
where
    T: Serialize + DeserializeOwned,
{
    let json = serde_json::to_value(&value).map_err(|error| {
        Error::conversion(format!(
            "Failed to serialize encrypted field '{}' before write: {}",
            encrypted_field_label(field_name, column_name),
            error
        ))
    })?;

    if json.is_null() {
        return Ok(value);
    }

    let encrypted =
        crate::types::__encrypt_json_value_for_attribute(&json, table_name, column_name)
            .map_err(|error| annotate_crypto_error(error, "encrypt", field_name, column_name))?;
    serde_json::from_value(serde_json::Value::String(encrypted)).map_err(|error| {
        Error::configuration(format!(
            "Encrypted field '{}' must use String/Text storage or Option<String>/Option<Text>: {}",
            encrypted_field_label(field_name, column_name),
            error
        ))
    })
}

pub(crate) fn decrypt_model_field<T>(
    value: T,
    table_name: &str,
    field_name: &str,
    column_name: &str,
) -> Result<T>
where
    T: Serialize + DeserializeOwned,
{
    let json = serde_json::to_value(&value).map_err(|error| {
        Error::conversion(format!(
            "Failed to inspect encrypted field '{}' after read: {}",
            encrypted_field_label(field_name, column_name),
            error
        ))
    })?;

    match json {
        serde_json::Value::Null => Ok(value),
        serde_json::Value::String(text) => {
            if !crate::types::__is_encrypted_json_value(&text) {
                return Err(unencrypted_field_data_error(field_name, column_name));
            }

            let decrypted =
                crate::types::__decrypt_json_value_for_attribute(&text, table_name, column_name)
                    .map_err(|error| {
                        annotate_crypto_error(error, "decrypt", field_name, column_name)
                    })?;
            serde_json::from_value(decrypted).map_err(|error| {
                Error::conversion(format!(
                    "Failed to deserialize decrypted field '{}': {}",
                    encrypted_field_label(field_name, column_name),
                    error
                ))
            })
        }
        _ => Err(Error::configuration(format!(
            "Encrypted field '{}' must load from a string or null database column",
            encrypted_field_label(field_name, column_name)
        ))),
    }
}

fn unencrypted_field_data_error(field_name: &str, column_name: &str) -> Error {
    Error::conversion(format!(
        "Encrypted field '{}' loaded plaintext data; expected a TideORM encrypted payload or null",
        encrypted_field_label(field_name, column_name)
    ))
}

pub(crate) fn prepare_batch_update_value<M: ModelMeta>(
    field_or_column: &str,
    value: UpdateValue,
) -> Result<UpdateValue> {
    let Some((field_name, column_name)) = resolve_encrypted_field::<M>(field_or_column) else {
        return Ok(value);
    };

    match value {
        UpdateValue::Value(value) => Ok(UpdateValue::Value(encrypt_batch_json_value(
            value,
            M::table_name(),
            field_name,
            column_name,
        )?)),
        UpdateValue::Coalesce(value) => Ok(UpdateValue::Coalesce(encrypt_batch_json_value(
            value,
            M::table_name(),
            field_name,
            column_name,
        )?)),
        UpdateValue::UnsafeRaw(_) => {
            unsupported_batch_operation("trusted raw SQL", field_name, column_name)
        }
        UpdateValue::Increment(_) => {
            unsupported_batch_operation("increment", field_name, column_name)
        }
        UpdateValue::Decrement(_) => {
            unsupported_batch_operation("decrement", field_name, column_name)
        }
        UpdateValue::Multiply(_) => {
            unsupported_batch_operation("multiply", field_name, column_name)
        }
        UpdateValue::Divide(_) => unsupported_batch_operation("divide", field_name, column_name),
        UpdateValue::ArrayAppend(_) => {
            unsupported_batch_operation("array append", field_name, column_name)
        }
        UpdateValue::ArrayRemove(_) => {
            unsupported_batch_operation("array remove", field_name, column_name)
        }
        UpdateValue::JsonSet(_, _) => {
            unsupported_batch_operation("json set", field_name, column_name)
        }
    }
}

fn resolve_encrypted_field<M: ModelMeta>(name: &str) -> Option<(&'static str, &'static str)> {
    let encrypted_fields = M::encrypted_fields();
    let encrypted_columns = M::encrypted_column_names();

    encrypted_fields
        .into_iter()
        .zip(encrypted_columns)
        .find(|(field_name, column_name)| *field_name == name || *column_name == name)
}

fn encrypt_batch_json_value(
    value: serde_json::Value,
    table_name: &str,
    field_name: &str,
    column_name: &str,
) -> Result<serde_json::Value> {
    if value.is_null() {
        return Ok(serde_json::Value::Null);
    }

    crate::types::__encrypt_json_value_for_attribute(&value, table_name, column_name)
        .map(serde_json::Value::String)
        .map_err(|error| annotate_crypto_error(error, "encrypt", field_name, column_name))
}

fn unsupported_batch_operation<T>(
    operation: &str,
    field_name: &str,
    column_name: &str,
) -> Result<T> {
    Err(Error::invalid_query(format!(
        "Batch operation '{}' is not supported for encrypted field '{}'",
        operation,
        encrypted_field_label(field_name, column_name)
    )))
}

fn encrypted_field_label(field_name: &str, column_name: &str) -> String {
    if field_name == column_name {
        field_name.to_string()
    } else {
        format!("{} ({})", field_name, column_name)
    }
}

fn annotate_crypto_error(
    error: Error,
    operation: &str,
    field_name: &str,
    column_name: &str,
) -> Error {
    let message = format!(
        "Failed to {} encrypted field '{}': {}",
        operation,
        encrypted_field_label(field_name, column_name),
        error
    );

    match error {
        Error::Configuration { .. } => Error::configuration(message),
        Error::Conversion { .. } => Error::conversion(message),
        Error::Query { .. } => Error::query(message),
        Error::Tokenization { .. } => Error::tokenization(message),
        Error::InvalidToken { .. } => Error::invalid_token(message),
        Error::Validation { field, .. } => Error::validation(field, message),
        Error::Connection { .. } => Error::connection(message),
        Error::Transaction { .. } => Error::transaction(message),
        Error::NotFound { .. } => Error::query(message),
        Error::BackendNotSupported { backend, .. } => {
            Error::backend_not_supported(message, backend)
        }
        Error::PrimaryKeyNotSet { model, .. } => Error::primary_key_not_set(message, model),
        Error::InsertReturningNotSupported { backend, .. } => {
            Error::insert_returning_not_supported(message, backend)
        }
        Error::Internal { .. } => Error::internal(message),
    }
}