Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled
rust / build / test / clippy / fmt (push) Has been cancelled
Layout:
- src/ .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
MxAsbClient, probes, tests, harnesses. Executable spec.
- design/ Architectural plan for the Rust port (M0–M6), error
model, protocol invariants, risks (R1–R16), adversarial
review log (review.md).
- rust/ Rust workspace. M0 skeleton + M1 codec parity.
mxaccess-codec: 215 unit tests + 2 cross-implementation
parity tests (byte-identical against .NET reference).
Other crates are M0 stubs awaiting M2+.
- captures/ Frida + netsh + pcap evidence per CLAUDE.md
("captures are evidence, not throwaway logs").
- analysis/ Decompiled C# (frida/proxy/decompiled-*),
Ghidra exports for native DLLs (`exports/` only —
working state at `projects/` and AVEVA's input
binaries at `input/` are gitignored).
- docs/ Reverse-engineering reference docs.
- tools/ Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/ Rust CI: fmt + build + test + clippy on Windows.
- LICENSE MIT (Joseph Doherty, 2026).
Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly
Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace MxNativeClient;
|
||||
|
||||
public sealed record GalaxyUserProfile(
|
||||
int UserProfileId,
|
||||
string UserProfileName,
|
||||
Guid UserGuid,
|
||||
string DefaultSecurityGroup,
|
||||
int? InTouchAccessLevel,
|
||||
IReadOnlyList<string> Roles);
|
||||
|
||||
public sealed class GalaxyRepositoryUserResolver
|
||||
{
|
||||
private const string DefaultConnectionString =
|
||||
"Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;TrustServerCertificate=True";
|
||||
|
||||
private readonly string _connectionString;
|
||||
|
||||
public GalaxyRepositoryUserResolver(string connectionString = DefaultConnectionString)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
|
||||
_connectionString = connectionString;
|
||||
}
|
||||
|
||||
public async Task<int> ResolveUserProfileIdByGuidAsync(
|
||||
Guid userGuid,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
GalaxyUserProfile profile = await ResolveByGuidAsync(userGuid, cancellationToken).ConfigureAwait(false);
|
||||
return profile.UserProfileId;
|
||||
}
|
||||
|
||||
public async Task<GalaxyUserProfile> ResolveByGuidAsync(
|
||||
Guid userGuid,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = UserByGuidSql;
|
||||
command.Parameters.AddWithValue("@userGuid", userGuid);
|
||||
|
||||
await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
throw new KeyNotFoundException($"Galaxy user GUID {userGuid} was not found in dbo.user_profile.");
|
||||
}
|
||||
|
||||
return ReadProfile(reader);
|
||||
}
|
||||
|
||||
public async Task<GalaxyUserProfile> ResolveByNameAsync(
|
||||
string userName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(userName);
|
||||
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = UserByNameSql;
|
||||
command.Parameters.AddWithValue("@userName", userName);
|
||||
|
||||
await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
throw new KeyNotFoundException($"Galaxy user {userName} was not found in dbo.user_profile.");
|
||||
}
|
||||
|
||||
return ReadProfile(reader);
|
||||
}
|
||||
|
||||
private static GalaxyUserProfile ReadProfile(SqlDataReader reader)
|
||||
{
|
||||
return new GalaxyUserProfile(
|
||||
reader.GetInt32(0),
|
||||
reader.GetString(1),
|
||||
reader.GetGuid(2),
|
||||
reader.GetString(3),
|
||||
reader.IsDBNull(4) ? null : reader.GetInt32(4),
|
||||
reader.IsDBNull(5) ? [] : ParseRoleBlob(reader.GetString(5)));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> ParseRoleBlob(string rolesText)
|
||||
{
|
||||
if (!rolesText.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || rolesText.Length <= 2)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
byte[] bytes = Convert.FromHexString(rolesText[2..]);
|
||||
List<string> roles = [];
|
||||
for (int offset = 0; offset + 3 < bytes.Length; offset++)
|
||||
{
|
||||
List<char> chars = [];
|
||||
int cursor = offset;
|
||||
while (cursor + 1 < bytes.Length)
|
||||
{
|
||||
char c = (char)(bytes[cursor] | (bytes[cursor + 1] << 8));
|
||||
if (c == '\0')
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (c < 0x20 || c > 0x7e)
|
||||
{
|
||||
chars.Clear();
|
||||
break;
|
||||
}
|
||||
|
||||
chars.Add(c);
|
||||
cursor += 2;
|
||||
}
|
||||
|
||||
if (chars.Count < 2 || cursor + 1 >= bytes.Length || bytes[cursor] != 0 || bytes[cursor + 1] != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string role = new(chars.ToArray());
|
||||
if (!roles.Contains(role, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
roles.Add(role);
|
||||
}
|
||||
|
||||
offset = cursor;
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
private const string UserSelectSql = """
|
||||
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
|
||||
""";
|
||||
|
||||
private const string UserByGuidSql = UserSelectSql + "\nWHERE user_guid = @userGuid\nORDER BY user_profile_id";
|
||||
|
||||
private const string UserByNameSql = UserSelectSql + "\nWHERE user_profile_name = @userName\nORDER BY user_profile_id";
|
||||
}
|
||||
Reference in New Issue
Block a user