From 8b63b51bec1db85e36d45fb9133ff7e31702ca02 Mon Sep 17 00:00:00 2001 From: Priec Date: Sun, 16 Aug 2026 16:52:28 +0200 Subject: [PATCH] type hardening --- Cargo.lock | 1 + Cargo.toml | 3 ++ client | 2 +- common/Cargo.toml | 1 + common/proto/tables_data.proto | 3 +- common/src/lib.rs | 1 + common/src/money.rs | 53 ++++++++++++++++++++++++ common/src/proto/descriptor.bin | Bin 183712 -> 183788 bytes common/src/proto/komp_ac.tables_data.rs | 3 +- server | 2 +- tui-canvas | 2 +- tui-pages | 2 +- 12 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 common/src/money.rs diff --git a/Cargo.lock b/Cargo.lock index a910a08c..53e223f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,6 +1288,7 @@ dependencies = [ "prost-build", "prost-types", "rust_decimal", + "rusty-money", "serde", "serde_json", "tantivy", diff --git a/Cargo.toml b/Cargo.toml index 506932ab..94ec1b9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,9 @@ tantivy = "0.26.1" # Steel_decimal crate rust_decimal = { version = "1.42.0", features = ["maths", "serde"] } 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" regex = "1.12.4" diff --git a/client b/client index e3b5d57c..520fb563 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit e3b5d57c905eb8b2a74d02d0332106e1ab990bad +Subproject commit 520fb563f4d90f411b6144a03cf6da230babfb30 diff --git a/common/Cargo.toml b/common/Cargo.toml index a6c2f4e1..cb94cdd9 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true [dependencies] prost-types = { workspace = true } rust_decimal = { workspace = true } +rusty-money = { workspace = true } tonic = "0.14.6" prost = "0.14.4" diff --git a/common/proto/tables_data.proto b/common/proto/tables_data.proto index 5009fca0..2be621c3 100644 --- a/common/proto/tables_data.proto +++ b/common/proto/tables_data.proto @@ -131,7 +131,8 @@ message PostTableDataRequest { // - BOOLEAN: bool value // - TIMESTAMPTZ: ISO 8601/RFC 3339 string (parsed to TIMESTAMPTZ) // - 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 // (numbers for NUMERIC are rejected to avoid precision loss) // diff --git a/common/src/lib.rs b/common/src/lib.rs index 07f6b4ea..e6427035 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -3,6 +3,7 @@ pub mod search; pub mod decimal; pub mod grpc_error; +pub mod money; pub mod relationship; pub mod system_column; diff --git a/common/src/money.rs b/common/src/money.rs new file mode 100644 index 00000000..e092cc95 --- /dev/null +++ b/common/src/money.rs @@ -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:?}" + ); + } + } +} diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 2180e340db3e1331f3fd9d9972811708c81ae915..13a93b58e53e287d27f8e2474bf83d0f07c8dae3 100644 GIT binary patch delta 5779 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 diff --git a/common/src/proto/komp_ac.tables_data.rs b/common/src/proto/komp_ac.tables_data.rs index 3b8b3c78..2fd878ab 100644 --- a/common/src/proto/komp_ac.tables_data.rs +++ b/common/src/proto/komp_ac.tables_data.rs @@ -27,7 +27,8 @@ pub struct PostTableDataRequest { /// * BOOLEAN: bool value /// * TIMESTAMPTZ: ISO 8601/RFC 3339 string (parsed to TIMESTAMPTZ) /// * 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 /// (numbers for NUMERIC are rejected to avoid precision loss) /// diff --git a/server b/server index 412af6ea..62be9cbb 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 412af6ea3f147bd79ae4984d0adfa60b37091a5c +Subproject commit 62be9cbbaf0f5a654083967601fc827ab035c7f7 diff --git a/tui-canvas b/tui-canvas index 1251d224..b1761279 160000 --- a/tui-canvas +++ b/tui-canvas @@ -1 +1 @@ -Subproject commit 1251d224067523abcd6d9107adf0d1a062693b6c +Subproject commit b1761279ff760cae62d7bf33f04311d444e5ea83 diff --git a/tui-pages b/tui-pages index 29e1e6a4..46afc191 160000 --- a/tui-pages +++ b/tui-pages @@ -1 +1 @@ -Subproject commit 29e1e6a42e2e9d76fac5a226098a52fd50ca30c0 +Subproject commit 46afc191dbd4cd9d047632401047255b6fc8e24d