[M3] mxaccess-galaxy: GalaxyUserProfile + UserResolver trait + role-blob
Lands the user-resolver half of M3 stream A. Pure-Rust foundation — the tiberius-backed SQL impl is logged as F14 and stays gated behind the existing galaxy-resolver Cargo feature. New - role_blob.rs (~270 LoC, 12 tests including a garbage-between-roles edge case) — port of ParseRoleBlob (cs:87-133). Sliding-window scan over hex-decoded UTF-16LE bytes; rejects non-printable code units; case-insensitive dedup. Pure function, no I/O. - user.rs (~290 LoC, 8 tests including 4 tokio-driven InMemoryUserResolver cases) — GalaxyUserProfile (port of cs:5-11) + from_columns helper bridging into role_blob + UserResolver async trait + UserResolverError with NotFound / Backend variants. - sql.rs additions: USER_SELECT_SQL + USER_BY_GUID_SQL + USER_BY_NAME_SQL constants (port of cs:135-148). Inline concatcp! macro composes the base SELECT with each WHERE clause at compile time without pulling const_format. Cargo.toml: added uuid (Galaxy user_guid is a uniqueidentifier). design/followups.md: added F14 (P2) for the tiberius-backed SQL impl behind the galaxy-resolver feature. Test count delta: 427 -> 446 (+19; mxaccess-galaxy 30 -> 49). All four DoD gates green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
// The `concatcp!` macro below uses fixed-size byte indexing in a `const fn`
|
||||
// where lengths are statically known. `.get(n)?` is not available in `const`
|
||||
// contexts in stable Rust 1.85, so the indexing is the only path. The
|
||||
// resulting `&'static str` constants are evaluated at compile time, so
|
||||
// any out-of-bounds would surface as a compile error rather than a runtime
|
||||
// panic.
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
//! SQL strings used by the future tiberius-backed resolver.
|
||||
//!
|
||||
//! Direct port of the two `private const string` blocks at
|
||||
@@ -172,6 +180,84 @@ FROM (
|
||||
ORDER BY CASE attribute_source WHEN N'dynamic' THEN 0 ELSE 1 END
|
||||
"#;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// User resolver SQL — port of GalaxyRepositoryUserResolver.cs:135-148.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Common `SELECT TOP (1) ...` user-profile projection — `cs:135-144`.
|
||||
/// Used as the base for both [`USER_BY_GUID_SQL`] and [`USER_BY_NAME_SQL`].
|
||||
///
|
||||
/// Result columns (in order):
|
||||
///
|
||||
/// 0. `user_profile_id` `int`
|
||||
/// 1. `user_profile_name` `nvarchar`
|
||||
/// 2. `user_guid` `uniqueidentifier`
|
||||
/// 3. `default_security_group` `nvarchar`
|
||||
/// 4. `intouch_access_level` `int` (NULL-able)
|
||||
/// 5. `roles_text` `nvarchar(max)` — `CONVERT(...)` of the
|
||||
/// `roles` `varbinary` column. Decode through
|
||||
/// [`crate::role_blob::parse_role_blob`].
|
||||
pub const USER_SELECT_SQL: &str = r#"SELECT TOP (1)
|
||||
user_profile_id,
|
||||
user_profile_name,
|
||||
user_guid,
|
||||
default_security_group,
|
||||
intouch_access_level,
|
||||
CONVERT(nvarchar(max), roles) AS roles_text
|
||||
FROM dbo.user_profile"#;
|
||||
|
||||
/// `Resolve user_profile by user_guid` — port of `cs:146`.
|
||||
///
|
||||
/// Parameter: `@userGuid` (`uniqueidentifier`).
|
||||
pub const USER_BY_GUID_SQL: &str = concatcp!(
|
||||
USER_SELECT_SQL,
|
||||
"\nWHERE user_guid = @userGuid\nORDER BY user_profile_id"
|
||||
);
|
||||
|
||||
/// `Resolve user_profile by user_profile_name` — port of `cs:148`.
|
||||
///
|
||||
/// Parameter: `@userName` (`nvarchar`).
|
||||
pub const USER_BY_NAME_SQL: &str = concatcp!(
|
||||
USER_SELECT_SQL,
|
||||
"\nWHERE user_profile_name = @userName\nORDER BY user_profile_id"
|
||||
);
|
||||
|
||||
/// Tiny `concat!`-equivalent for `&'static str` constants, since `concat!`
|
||||
/// only works with literals. Two-arg specialisation; keeps `USER_BY_GUID_SQL`
|
||||
/// and `USER_BY_NAME_SQL` evaluable at compile time without dragging in
|
||||
/// `const_format` as a dep.
|
||||
macro_rules! concatcp {
|
||||
($a:expr, $b:expr) => {{
|
||||
const A: &str = $a;
|
||||
const B: &str = $b;
|
||||
const N: usize = A.len() + B.len();
|
||||
const fn build() -> [u8; N] {
|
||||
let mut out = [0u8; N];
|
||||
let a = A.as_bytes();
|
||||
let b = B.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < a.len() {
|
||||
out[i] = a[i];
|
||||
i += 1;
|
||||
}
|
||||
let mut j = 0;
|
||||
while j < b.len() {
|
||||
out[a.len() + j] = b[j];
|
||||
j += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
// SAFETY: A and B are valid UTF-8 (both are `&'static str`); the
|
||||
// concatenation of two valid UTF-8 byte sequences is valid UTF-8.
|
||||
const COMBINED: &[u8; N] = &build();
|
||||
match core::str::from_utf8(COMBINED) {
|
||||
Ok(s) => s,
|
||||
Err(_) => "",
|
||||
}
|
||||
}};
|
||||
}
|
||||
pub(crate) use concatcp;
|
||||
|
||||
/// Multi-row browse query — `Browse(object_tag_like, attribute_like, max_rows)`.
|
||||
///
|
||||
/// Parameters:
|
||||
|
||||
Reference in New Issue
Block a user