type hardening

This commit is contained in:
Priec
2026-08-16 16:52:28 +02:00
parent 99212252b2
commit 8b63b51bec
12 changed files with 67 additions and 6 deletions

1
Cargo.lock generated
View File

@@ -1288,6 +1288,7 @@ dependencies = [
"prost-build", "prost-build",
"prost-types", "prost-types",
"rust_decimal", "rust_decimal",
"rusty-money",
"serde", "serde",
"serde_json", "serde_json",
"tantivy", "tantivy",

View File

@@ -43,6 +43,9 @@ tantivy = "0.26.1"
# Steel_decimal crate # Steel_decimal crate
rust_decimal = { version = "1.42.0", features = ["maths", "serde"] } rust_decimal = { version = "1.42.0", features = ["maths", "serde"] }
rust_decimal_macros = "1.40.0" rust_decimal_macros = "1.40.0"
# One version across the workspace: `&'static iso::Currency` only crosses crate
# boundaries if every crate resolves rusty-money to the same instance.
rusty-money = "0.5.0"
thiserror = "2.0.18" thiserror = "2.0.18"
regex = "1.12.4" regex = "1.12.4"

2
client

Submodule client updated: e3b5d57c90...520fb563f4

View File

@@ -7,6 +7,7 @@ license.workspace = true
[dependencies] [dependencies]
prost-types = { workspace = true } prost-types = { workspace = true }
rust_decimal = { workspace = true } rust_decimal = { workspace = true }
rusty-money = { workspace = true }
tonic = "0.14.6" tonic = "0.14.6"
prost = "0.14.4" prost = "0.14.4"

View File

@@ -131,7 +131,8 @@ message PostTableDataRequest {
// - BOOLEAN: bool value // - BOOLEAN: bool value
// - TIMESTAMPTZ: ISO 8601/RFC 3339 string (parsed to TIMESTAMPTZ) // - TIMESTAMPTZ: ISO 8601/RFC 3339 string (parsed to TIMESTAMPTZ)
// - INTEGER: number with no fractional part and within i32 range // - INTEGER: number with no fractional part and within i32 range
// - BIGINT: number with no fractional part and within i64 range // - BIGINT: canonical integer string for the full i64 range; a number is
// accepted only within protobuf NumberValue's exact integer range
// - NUMERIC(p,s): string representation only; empty string becomes NULL // - NUMERIC(p,s): string representation only; empty string becomes NULL
// (numbers for NUMERIC are rejected to avoid precision loss) // (numbers for NUMERIC are rejected to avoid precision loss)
// //

View File

@@ -3,6 +3,7 @@
pub mod search; pub mod search;
pub mod decimal; pub mod decimal;
pub mod grpc_error; pub mod grpc_error;
pub mod money;
pub mod relationship; pub mod relationship;
pub mod system_column; pub mod system_column;

53
common/src/money.rs Normal file
View File

@@ -0,0 +1,53 @@
// common/src/money.rs
//!
//! The one canonical currency spelling shared by the client and the server.
//!
//! A currency code is stored, sent and compared as its canonical uppercase
//! ISO-4217 alphabetic code. Both ends resolve it with
//! [`require_iso_currency_code`], so the client rejects exactly what the server
//! would reject, and each side renders the shared error as its own error type.
pub use rusty_money::iso;
/// Resolves a canonical uppercase ISO-4217 alphabetic code to its currency.
///
/// Rejects anything not already canonical: lowercase (`eur`), surrounding
/// whitespace, and any length but three. Normalising here instead would let a
/// value be written in one spelling and compared in another.
pub fn require_iso_currency_code(currency_code: &str) -> Result<&'static iso::Currency, String> {
if currency_code.len() != 3
|| !currency_code
.chars()
.all(|character| character.is_ascii_uppercase())
{
return Err("Currency must be a canonical uppercase ISO-4217 code".to_string());
}
iso::find(currency_code).ok_or_else(|| format!("Unknown ISO-4217 currency: {currency_code}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_codes_resolve_to_their_currency() {
assert_eq!(require_iso_currency_code("EUR").unwrap(), iso::EUR);
assert_eq!(
require_iso_currency_code("USD").unwrap().iso_alpha_code,
"USD"
);
}
#[test]
fn non_canonical_or_unknown_codes_are_rejected() {
for currency_code in [
"", "E", "EU", "EURO", "eur", "Eur", " EUR", "EUR ", "E R", "E\u{20AC}R", "AAA", "123",
] {
assert!(
require_iso_currency_code(currency_code).is_err(),
"unexpectedly accepted {currency_code:?}"
);
}
}
}

Binary file not shown.

View File

@@ -27,7 +27,8 @@ pub struct PostTableDataRequest {
/// * BOOLEAN: bool value /// * BOOLEAN: bool value
/// * TIMESTAMPTZ: ISO 8601/RFC 3339 string (parsed to TIMESTAMPTZ) /// * TIMESTAMPTZ: ISO 8601/RFC 3339 string (parsed to TIMESTAMPTZ)
/// * INTEGER: number with no fractional part and within i32 range /// * INTEGER: number with no fractional part and within i32 range
/// * BIGINT: number with no fractional part and within i64 range /// * BIGINT: canonical integer string for the full i64 range; a number is
/// accepted only within protobuf NumberValue's exact integer range
/// * NUMERIC(p,s): string representation only; empty string becomes NULL /// * NUMERIC(p,s): string representation only; empty string becomes NULL
/// (numbers for NUMERIC are rejected to avoid precision loss) /// (numbers for NUMERIC are rejected to avoid precision loss)
/// ///

2
server

Submodule server updated: 412af6ea3f...62be9cbbaf