Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of RunicPortals v1.1.3
RunicPortals.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Text; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using RunicPermissions.Contracts; using RunicPermissions.Groups; using RunicPortals.Api; using RunicPortals.Core; using RunicPortals.Integration; using Splatform; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Runic Portals")] [assembly: AssemblyDescription("Permission-aware deterministic portal directories and fail-closed universal routing.")] [assembly: AssemblyCompany("Chazman")] [assembly: AssemblyProduct("Runic Portals")] [assembly: AssemblyCopyright("Copyright © 2026 Chazman")] [assembly: ComVisible(false)] [assembly: Guid("10c99bf6-a049-48d3-bcb3-4582c10180db")] [assembly: AssemblyFileVersion("1.1.3.0")] [assembly: AssemblyInformationalVersion("1.1.3")] [assembly: InternalsVisibleTo("RunicPortals.Tests")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("1.1.3.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace RunicPermissions.Contracts { public sealed class StableIdentity : IEquatable<StableIdentity>, IComparable<StableIdentity> { public const int MaximumAuthorityLength = 64; public const int MaximumSubjectIdLength = 256; public string Authority { get; } public string SubjectId { get; } public string CanonicalKey { get; } public StableIdentity(string authority, string subjectId) { string value = NormalizeAuthority(authority); string value2 = NormalizeSubject(subjectId); if (!IsValidAuthority(value)) { throw new ArgumentException("Identity authority must contain only ASCII letters, digits, '.', '_', or '-'.", "authority"); } if (!IsValidSubject(value2)) { throw new ArgumentException("Identity subject ID is empty, too long, or contains control characters.", "subjectId"); } Authority = value; SubjectId = value2; CanonicalKey = Authority + ":" + Uri.EscapeDataString(SubjectId); } public static bool TryCreate(string authority, string subjectId, out StableIdentity identity) { try { identity = new StableIdentity(authority, subjectId); return true; } catch (ArgumentException) { identity = null; return false; } } public bool Equals(StableIdentity other) { if (other != null && string.Equals(Authority, other.Authority, StringComparison.Ordinal)) { return string.Equals(SubjectId, other.SubjectId, StringComparison.Ordinal); } return false; } public override bool Equals(object obj) { return Equals(obj as StableIdentity); } public override int GetHashCode() { return (StringComparer.Ordinal.GetHashCode(Authority) * 397) ^ StringComparer.Ordinal.GetHashCode(SubjectId); } public int CompareTo(StableIdentity other) { if (other == null) { return 1; } int num = string.Compare(Authority, other.Authority, StringComparison.Ordinal); if (num == 0) { return string.Compare(SubjectId, other.SubjectId, StringComparison.Ordinal); } return num; } public override string ToString() { return CanonicalKey; } private static string NormalizeAuthority(string value) { return (value ?? string.Empty).Trim().ToLowerInvariant(); } private static string NormalizeSubject(string value) { return (value ?? string.Empty).Trim(); } private static bool IsValidAuthority(string value) { if (value.Length == 0 || value.Length > 64) { return false; } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '.' && c != '_' && c != '-') { return false; } } return true; } private static bool IsValidSubject(string value) { if (value.Length == 0 || value.Length > 256) { return false; } for (int i = 0; i < value.Length; i++) { if (char.IsControl(value[i])) { return false; } } return true; } } public enum IdentityResolutionStatus { Verified, Missing, Ambiguous, Stale } public sealed class IdentityClaim { public StableIdentity Identity { get; } public string DisplayNameSnapshot { get; } public IdentityResolutionStatus Status { get; } public bool IsVerified { get { if (Status == IdentityResolutionStatus.Verified) { return Identity != null; } return false; } } public IdentityClaim(StableIdentity identity, string displayNameSnapshot, IdentityResolutionStatus status) { Identity = identity; DisplayNameSnapshot = displayNameSnapshot ?? string.Empty; Status = status; } public static IdentityClaim Verified(StableIdentity identity, string displayNameSnapshot = "") { return new IdentityClaim(identity, displayNameSnapshot, IdentityResolutionStatus.Verified); } } } namespace RunicPermissions.Groups { public enum GroupWorldReadState { Missing, Ready, Corrupt, EvidenceConflict, Unavailable } public enum GroupWorldCommitState { Committed, RevisionConflict, Corrupt, EvidenceConflict, Unavailable, InvalidReplacement } public sealed class GroupWorldReadResult { public GroupWorldReadState State { get; } public string ReasonCode { get; } public GroupCatalog Catalog { get; } public string ExactSha256 { get; } internal GroupWorldReadResult(GroupWorldReadState state, string reasonCode, GroupCatalog catalog, string exactSha256) { State = state; ReasonCode = reasonCode ?? string.Empty; Catalog = catalog; ExactSha256 = exactSha256 ?? string.Empty; } } public sealed class GroupWorldCommitResult { public GroupWorldCommitState State { get; } public string ReasonCode { get; } public GroupWorldReadResult Current { get; } public bool Success => State == GroupWorldCommitState.Committed; internal GroupWorldCommitResult(GroupWorldCommitState state, string reasonCode, GroupWorldReadResult current) { State = state; ReasonCode = reasonCode ?? string.Empty; Current = current; } } public interface IGroupWorldStore { GroupWorldReadResult Read(string worldScope); GroupWorldCommitResult TryCommit(string worldScope, long expectedCatalogRevision, GroupCatalog replacement); } internal sealed class CompatibleGroupWorldStore : IGroupWorldStore { private readonly struct Paths { internal string Primary { get; } internal string Temporary { get; } internal string Backup { get; } internal string Lock { get; } internal Paths(string primary, string temporary, string backup, string @lock) { Primary = primary; Temporary = temporary; Backup = backup; Lock = @lock; } } private readonly string _root; internal CompatibleGroupWorldStore(string root) { if (string.IsNullOrWhiteSpace(root)) { throw new ArgumentException("A storage root is required.", "root"); } _root = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); if (string.Equals(_root, Path.GetPathRoot(_root), PathComparison())) { throw new ArgumentException("A filesystem root cannot be used.", "root"); } } public GroupWorldReadResult Read(string worldScope) { try { Paths paths = Resolve(worldScope); if (!Directory.Exists(_root)) { return Missing(); } using (Acquire(paths.Lock)) { return ReadLocked(paths, worldScope); } } catch (ArgumentException) { return Unavailable("group-world-scope-invalid"); } catch (Exception exception) when (StorageFailure(exception)) { return Unavailable("group-store-read-unavailable"); } } public GroupWorldCommitResult TryCommit(string worldScope, long expectedCatalogRevision, GroupCatalog replacement) { if (expectedCatalogRevision < 0 || replacement == null || replacement.Revision != expectedCatalogRevision + 1) { return Commit(GroupWorldCommitState.InvalidReplacement, "group-store-replacement-invalid", null); } try { Paths paths = Resolve(worldScope); Directory.CreateDirectory(_root); using (Acquire(paths.Lock)) { GroupWorldReadResult groupWorldReadResult = ReadLocked(paths, worldScope); if (groupWorldReadResult.State == GroupWorldReadState.Corrupt) { return Commit(GroupWorldCommitState.Corrupt, groupWorldReadResult.ReasonCode, groupWorldReadResult); } if (groupWorldReadResult.State == GroupWorldReadState.EvidenceConflict) { return Commit(GroupWorldCommitState.EvidenceConflict, groupWorldReadResult.ReasonCode, groupWorldReadResult); } if (groupWorldReadResult.State == GroupWorldReadState.Unavailable) { return Commit(GroupWorldCommitState.Unavailable, groupWorldReadResult.ReasonCode, groupWorldReadResult); } if (((groupWorldReadResult.State == GroupWorldReadState.Missing) ? 0 : groupWorldReadResult.Catalog.Revision) != expectedCatalogRevision) { return Commit(GroupWorldCommitState.RevisionConflict, "group-store-revision-conflict", groupWorldReadResult); } if (!SameLedger((groupWorldReadResult.State == GroupWorldReadState.Missing) ? GroupCommandLedger.Empty : groupWorldReadResult.Catalog.CommandLedger, replacement.CommandLedger)) { return Commit(GroupWorldCommitState.InvalidReplacement, "group-store-command-ledger-mismatch", groupWorldReadResult); } return Publish(paths, worldScope, replacement, groupWorldReadResult); } } catch (ArgumentException) { return Commit(GroupWorldCommitState.InvalidReplacement, "group-store-replacement-invalid", null); } catch (Exception exception) when (StorageFailure(exception)) { return Commit(GroupWorldCommitState.Unavailable, "group-store-commit-unavailable", null); } } private GroupWorldCommitResult Publish(Paths paths, string worldScope, GroupCatalog replacement, GroupWorldReadResult current) { if (File.Exists(paths.Temporary) || File.Exists(paths.Backup)) { return Commit(GroupWorldCommitState.EvidenceConflict, "group-store-nonprimary-evidence", current); } byte[] array = GroupCatalogCodec.Encode(worldScope, replacement); using (FileStream fileStream = new FileStream(paths.Temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)) { fileStream.Write(array, 0, array.Length); fileStream.Flush(flushToDisk: true); } byte[] array2 = ReadBounded(paths.Temporary); if (!Exact(array, array2) || !GroupCatalogCodec.TryDecode(array2, worldScope, out var catalog, out var _) || catalog.Revision != replacement.Revision) { return Commit(GroupWorldCommitState.Unavailable, "group-store-staged-readback-failed", current); } if (File.Exists(paths.Primary)) { File.Replace(paths.Temporary, paths.Primary, null, ignoreMetadataErrors: true); } else { File.Move(paths.Temporary, paths.Primary); } GroupWorldReadResult groupWorldReadResult = ReadPrimary(paths.Primary, worldScope); if (groupWorldReadResult.State != GroupWorldReadState.Ready || groupWorldReadResult.Catalog.Revision != replacement.Revision) { return Commit(GroupWorldCommitState.Unavailable, "group-store-commit-readback-failed", groupWorldReadResult); } return Commit(GroupWorldCommitState.Committed, "group-store-committed", groupWorldReadResult); } private GroupWorldReadResult ReadLocked(Paths paths, string worldScope) { if (File.Exists(paths.Temporary) || File.Exists(paths.Backup)) { return new GroupWorldReadResult(GroupWorldReadState.EvidenceConflict, "group-store-nonprimary-evidence", null, string.Empty); } if (!File.Exists(paths.Primary)) { return Missing(); } return ReadPrimary(paths.Primary, worldScope); } private static GroupWorldReadResult ReadPrimary(string path, string worldScope) { byte[] bytes = ReadBounded(path); if (!GroupCatalogCodec.TryDecode(bytes, worldScope, out var catalog, out var reason)) { return new GroupWorldReadResult(GroupWorldReadState.Corrupt, reason, null, GroupCatalogCodec.ComputeSha256(bytes)); } return new GroupWorldReadResult(GroupWorldReadState.Ready, "group-store-ready", catalog, GroupCatalogCodec.ComputeSha256(bytes)); } private Paths Resolve(string worldScope) { string s = GroupIdentity.RequireWorldScope(worldScope); byte[] array; using (SHA256 sHA = SHA256.Create()) { array = sHA.ComputeHash(Encoding.UTF8.GetBytes(s)); } StringBuilder stringBuilder = new StringBuilder(array.Length * 2); for (int i = 0; i < array.Length; i++) { stringBuilder.Append(array[i].ToString("x2")); } string text = Path.Combine(_root, stringBuilder?.ToString() + ".groups"); if (!string.Equals(Path.GetDirectoryName(text), _root, PathComparison())) { throw new ArgumentException("The group path escaped its root.", "worldScope"); } return new Paths(text, text + ".tmp", text + ".bak", text + ".lock"); } private static FileStream Acquire(string path) { return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, 1, FileOptions.WriteThrough); } private static byte[] ReadBounded(string path) { using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.SequentialScan); if (fileStream.Length < 1 || fileStream.Length > 8388608) { throw new InvalidDataException("The group catalog length is invalid."); } byte[] array = new byte[(int)fileStream.Length]; int num; for (int i = 0; i < array.Length; i += num) { num = fileStream.Read(array, i, array.Length - i); if (num <= 0) { throw new EndOfStreamException(); } } return array; } private static bool SameLedger(GroupCommandLedger left, GroupCommandLedger right) { if (left != null && right != null && left.Epoch == right.Epoch && left.NextSequence == right.NextSequence && left.MinimumAcceptedSequence == right.MinimumAcceptedSequence && left.Issues.Count == right.Issues.Count) { return left.Receipts.Count == right.Receipts.Count; } return false; } private static bool Exact(byte[] left, byte[] right) { if (left == null || right == null || left.Length != right.Length) { return false; } int num = 0; for (int i = 0; i < left.Length; i++) { num |= left[i] ^ right[i]; } return num == 0; } private static bool StorageFailure(Exception exception) { if (!(exception is IOException) && !(exception is UnauthorizedAccessException) && !(exception is NotSupportedException)) { return exception is SecurityException; } return true; } private static StringComparison PathComparison() { if (Path.DirectorySeparatorChar != '\\') { return StringComparison.Ordinal; } return StringComparison.OrdinalIgnoreCase; } private static GroupWorldReadResult Missing() { return new GroupWorldReadResult(GroupWorldReadState.Missing, "group-store-missing", GroupCatalog.Empty, string.Empty); } private static GroupWorldReadResult Unavailable(string reason) { return new GroupWorldReadResult(GroupWorldReadState.Unavailable, reason, null, string.Empty); } private static GroupWorldCommitResult Commit(GroupWorldCommitState state, string reason, GroupWorldReadResult current) { return new GroupWorldCommitResult(state, reason, current); } } public enum GroupRole : byte { Member = 1, Officer, Owner } public static class GroupLimits { public const int MaximumGroups = 256; public const int MaximumMembersPerGroup = 256; public const int MaximumInvitationsPerGroup = 256; public const int MaximumGroupsPerIdentity = 64; public const int MaximumRetiredGroupIds = 4096; public const int MaximumDisplayNameUtf8Bytes = 64; public const int MaximumWorldScopeUtf8Bytes = 128; public const int MaximumCatalogBytes = 8388608; public static readonly TimeSpan MaximumInvitationLifetime = TimeSpan.FromDays(30.0); } public static class GroupIdentity { public static string ToCanonicalId(Guid groupId) { if (groupId == Guid.Empty) { throw new ArgumentException("A nonempty group UUID is required.", "groupId"); } return groupId.ToString("N"); } public static bool TryParseCanonicalId(string value, out Guid groupId) { groupId = Guid.Empty; if (value != null && value.Length == 32 && Guid.TryParseExact(value, "N", out groupId) && groupId != Guid.Empty) { return string.Equals(value, groupId.ToString("N"), StringComparison.Ordinal); } return false; } public static bool IsCanonicalId(string value) { Guid groupId; return TryParseCanonicalId(value, out groupId); } internal static string RequireDisplayName(string value) { if (value == null || value.Length == 0 || !string.Equals(value, value.Trim(), StringComparison.Ordinal) || !value.IsNormalized(NormalizationForm.FormC) || Encoding.UTF8.GetByteCount(value) > 64) { throw new ArgumentException("A nonempty, trimmed, NFC group display name within 64 UTF-8 bytes is required.", "value"); } for (int i = 0; i < value.Length; i++) { if (char.IsControl(value[i])) { throw new ArgumentException("Group display names cannot contain control characters.", "value"); } } return value; } internal static string RequireWorldScope(string value) { if (value == null || value.Length == 0 || value.Length > 128 || Encoding.UTF8.GetByteCount(value) > 128) { throw new ArgumentException("A bounded canonical world scope is required.", "value"); } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '.' && c != '_' && c != '-') { throw new ArgumentException("The world scope is not canonical.", "value"); } } return value; } } public sealed class GroupMember { public StableIdentity Identity { get; } public GroupRole Role { get; } public long JoinedRevision { get; } public GroupMember(StableIdentity identity, GroupRole role, long joinedRevision) { Identity = identity ?? throw new ArgumentNullException("identity"); if (!Enum.IsDefined(typeof(GroupRole), role)) { throw new ArgumentOutOfRangeException("role"); } if (joinedRevision < 1) { throw new ArgumentOutOfRangeException("joinedRevision"); } Role = role; JoinedRevision = joinedRevision; } } public sealed class GroupInvitation { public StableIdentity Invitee { get; } public StableIdentity InvitedBy { get; } public long IssuedRevision { get; } public long ExpiresUtcTicks { get; } public GroupInvitation(StableIdentity invitee, StableIdentity invitedBy, long issuedRevision, long expiresUtcTicks) { Invitee = invitee ?? throw new ArgumentNullException("invitee"); InvitedBy = invitedBy ?? throw new ArgumentNullException("invitedBy"); if (issuedRevision < 1) { throw new ArgumentOutOfRangeException("issuedRevision"); } if (expiresUtcTicks <= 0) { throw new ArgumentOutOfRangeException("expiresUtcTicks"); } IssuedRevision = issuedRevision; ExpiresUtcTicks = expiresUtcTicks; } public bool IsExpired(long nowUtcTicks) { if (nowUtcTicks > 0) { return nowUtcTicks >= ExpiresUtcTicks; } return true; } } public sealed class GroupRecord { private readonly GroupMember[] _members; private readonly GroupInvitation[] _invitations; private readonly ReadOnlyCollection<GroupMember> _memberView; private readonly ReadOnlyCollection<GroupInvitation> _invitationView; public Guid Id { get; } public string IdText { get; } public string DisplayName { get; } public long Revision { get; } public IReadOnlyList<GroupMember> Members => _memberView; public IReadOnlyList<GroupInvitation> Invitations => _invitationView; public GroupRecord(Guid id, string displayName, long revision, IEnumerable<GroupMember> members, IEnumerable<GroupInvitation> invitations = null) { Id = id; IdText = GroupIdentity.ToCanonicalId(id); DisplayName = GroupIdentity.RequireDisplayName(displayName); if (revision < 1) { throw new ArgumentOutOfRangeException("revision"); } Revision = revision; _members = CopyMembers(members, revision); _invitations = CopyInvitations(invitations, revision, _members); _memberView = Array.AsReadOnly(_members); _invitationView = Array.AsReadOnly(_invitations); } public bool TryGetMember(StableIdentity identity, out GroupMember member) { member = null; if (identity == null) { return false; } int num = FindMember(_members, identity); if (num < 0) { return false; } member = _members[num]; return true; } public bool TryGetInvitation(StableIdentity identity, out GroupInvitation invitation) { invitation = null; if (identity == null) { return false; } int num = FindInvitation(_invitations, identity); if (num < 0) { return false; } invitation = _invitations[num]; return true; } internal GroupRecord Rename(string displayName) { return new GroupRecord(Id, displayName, checked(Revision + 1), _members, _invitations); } internal GroupRecord Invite(StableIdentity actor, StableIdentity invitee, long expiresUtcTicks) { checked { List<GroupInvitation> invitations = new List<GroupInvitation>(_invitations) { new GroupInvitation(invitee, actor, Revision + 1, expiresUtcTicks) }; return new GroupRecord(Id, DisplayName, Revision + 1, _members, invitations); } } internal GroupRecord CancelInvitation(StableIdentity invitee) { GroupInvitation[] invitations = _invitations.Where((GroupInvitation value) => !value.Invitee.Equals(invitee)).ToArray(); return new GroupRecord(Id, DisplayName, checked(Revision + 1), _members, invitations); } internal GroupRecord Accept(StableIdentity invitee) { long num = checked(Revision + 1); List<GroupMember> members = new List<GroupMember>(_members) { new GroupMember(invitee, GroupRole.Member, num) }; GroupInvitation[] invitations = _invitations.Where((GroupInvitation value) => !value.Invitee.Equals(invitee)).ToArray(); return new GroupRecord(Id, DisplayName, num, members, invitations); } internal GroupRecord RemoveMember(StableIdentity identity) { GroupMember[] members = _members.Where((GroupMember value) => !value.Identity.Equals(identity)).ToArray(); return new GroupRecord(Id, DisplayName, checked(Revision + 1), members, _invitations); } internal GroupRecord SetRole(StableIdentity identity, GroupRole role) { GroupMember[] array = new GroupMember[_members.Length]; for (int i = 0; i < _members.Length; i++) { GroupMember groupMember = _members[i]; array[i] = (groupMember.Identity.Equals(identity) ? new GroupMember(groupMember.Identity, role, groupMember.JoinedRevision) : groupMember); } return new GroupRecord(Id, DisplayName, checked(Revision + 1), array, _invitations); } internal GroupRecord TransferOwnership(StableIdentity owner, StableIdentity successor) { GroupMember[] array = new GroupMember[_members.Length]; for (int i = 0; i < _members.Length; i++) { GroupMember groupMember = _members[i]; GroupRole groupRole = (groupMember.Identity.Equals(owner) ? GroupRole.Officer : (groupMember.Identity.Equals(successor) ? GroupRole.Owner : groupMember.Role)); array[i] = ((groupRole == groupMember.Role) ? groupMember : new GroupMember(groupMember.Identity, groupRole, groupMember.JoinedRevision)); } return new GroupRecord(Id, DisplayName, checked(Revision + 1), array, _invitations); } internal GroupRecord PruneExpired(long nowUtcTicks) { GroupInvitation[] array = _invitations.Where((GroupInvitation value) => !value.IsExpired(nowUtcTicks)).ToArray(); if (array.Length != _invitations.Length) { return new GroupRecord(Id, DisplayName, checked(Revision + 1), _members, array); } return this; } private static GroupMember[] CopyMembers(IEnumerable<GroupMember> source, long revision) { if (source == null) { throw new ArgumentNullException("source"); } GroupMember[] array = source.ToArray(); if (array.Length < 1 || array.Length > 256 || array.Any((GroupMember value) => value == null || value.JoinedRevision > revision)) { throw new ArgumentOutOfRangeException("source"); } Array.Sort(array, (GroupMember left, GroupMember right) => left.Identity.CompareTo(right.Identity)); int num = 0; for (int num2 = 0; num2 < array.Length; num2++) { if (num2 > 0 && array[num2 - 1].Identity.Equals(array[num2].Identity)) { throw new ArgumentException("Group member identities must be unique.", "source"); } if (array[num2].Role == GroupRole.Owner) { num++; } } if (num != 1) { throw new ArgumentException("A group requires exactly one Owner.", "source"); } return array; } private static GroupInvitation[] CopyInvitations(IEnumerable<GroupInvitation> source, long revision, IReadOnlyList<GroupMember> members) { GroupInvitation[] array = (source ?? Array.Empty<GroupInvitation>()).ToArray(); if (array.Length > 256 || array.Any((GroupInvitation value) => value == null || value.IssuedRevision > revision)) { throw new ArgumentOutOfRangeException("source"); } Array.Sort(array, (GroupInvitation left, GroupInvitation right) => left.Invitee.CompareTo(right.Invitee)); for (int num = 0; num < array.Length; num++) { if (num > 0 && array[num - 1].Invitee.Equals(array[num].Invitee)) { throw new ArgumentException("Group invitation identities must be unique.", "source"); } if (FindMember(members, array[num].Invitee) >= 0) { throw new ArgumentException("Group invitation membership evidence is invalid.", "source"); } } return array; } internal static int FindMember(IReadOnlyList<GroupMember> values, StableIdentity identity) { int num = 0; int num2 = values.Count - 1; while (num <= num2) { int num3 = num + (num2 - num) / 2; int num4 = values[num3].Identity.CompareTo(identity); if (num4 == 0) { return num3; } if (num4 < 0) { num = num3 + 1; } else { num2 = num3 - 1; } } return -1; } private static int FindInvitation(IReadOnlyList<GroupInvitation> values, StableIdentity identity) { int num = 0; int num2 = values.Count - 1; while (num <= num2) { int num3 = num + (num2 - num) / 2; int num4 = values[num3].Invitee.CompareTo(identity); if (num4 == 0) { return num3; } if (num4 < 0) { num = num3 + 1; } else { num2 = num3 - 1; } } return -1; } } public sealed class RetiredGroupId { public Guid Id { get; } public string IdText { get; } public long DeletedCatalogRevision { get; } public RetiredGroupId(Guid id, long deletedCatalogRevision) { Id = id; IdText = GroupIdentity.ToCanonicalId(id); if (deletedCatalogRevision < 1) { throw new ArgumentOutOfRangeException("deletedCatalogRevision"); } DeletedCatalogRevision = deletedCatalogRevision; } } internal static class GroupCommandDurabilityLimits { internal const int MaximumOutstandingIssues = 256; internal const int MaximumRetainedReceipts = 4096; internal const int MaximumReasonCharacters = 96; internal static readonly TimeSpan MaximumIssueLifetime = TimeSpan.FromMinutes(10.0); } internal sealed class GroupCommandIssue { internal Guid Epoch { get; } internal long Sequence { get; } internal StableIdentity Actor { get; } internal string RequestSha256 { get; } internal Guid GroupId { get; } internal long ExpectedCatalogRevision { get; } internal long ExpectedGroupRevision { get; } internal long ExpiresUtcTicks { get; } internal string Token => GroupCommandToken.Format(Epoch, Sequence); internal GroupCommandIssue(Guid epoch, long sequence, StableIdentity actor, string requestSha256, Guid groupId, long expectedCatalogRevision, long expectedGroupRevision, long expiresUtcTicks) { if (epoch == Guid.Empty || sequence < 1 || actor == null || groupId == Guid.Empty || expectedCatalogRevision < 0 || expectedGroupRevision < -1 || expiresUtcTicks <= 0) { throw new ArgumentException("The Group command issue is invalid."); } Epoch = epoch; Sequence = sequence; Actor = actor; RequestSha256 = RequireSha256(requestSha256); GroupId = groupId; ExpectedCatalogRevision = expectedCatalogRevision; ExpectedGroupRevision = expectedGroupRevision; ExpiresUtcTicks = expiresUtcTicks; } internal bool Matches(StableIdentity actor, string requestSha256) { if (actor != null && Actor.Equals(actor)) { return string.Equals(RequestSha256, requestSha256, StringComparison.Ordinal); } return false; } internal static string RequireSha256(string value) { if (value == null || value.Length != 64) { throw new ArgumentException("SHA-256 is invalid."); } foreach (char c in value) { if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { throw new ArgumentException("SHA-256 is not canonical lowercase hexadecimal."); } } return value; } } internal sealed class GroupCommandReceipt { internal Guid Epoch { get; } internal long Sequence { get; } internal StableIdentity Actor { get; } internal string RequestSha256 { get; } internal GroupMutationCode Code { get; } internal string ReasonCode { get; } internal long ExpectedCatalogRevision { get; } internal long ExpectedGroupRevision { get; } internal long CatalogRevision { get; } internal long GroupRevision { get; } internal string Token => GroupCommandToken.Format(Epoch, Sequence); internal bool Success { get { if (Code >= GroupMutationCode.Created) { return Code <= GroupMutationCode.NoChange; } return false; } } internal GroupCommandReceipt(Guid epoch, long sequence, StableIdentity actor, string requestSha256, GroupMutationCode code, string reasonCode, long expectedCatalogRevision, long expectedGroupRevision, long catalogRevision, long groupRevision) { if (epoch == Guid.Empty || sequence < 1 || actor == null || !Enum.IsDefined(typeof(GroupMutationCode), code) || expectedCatalogRevision < 0 || expectedGroupRevision < -1 || catalogRevision < 0 || groupRevision < -1) { throw new ArgumentException("The Group command receipt is invalid."); } string text = reasonCode ?? string.Empty; if (text.Length < 1 || text.Length > 96) { throw new ArgumentOutOfRangeException("reasonCode"); } foreach (char c in text) { if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '.' && c != '-' && c != '_') { throw new ArgumentException("The Group receipt reason is not canonical."); } } Epoch = epoch; Sequence = sequence; Actor = actor; RequestSha256 = GroupCommandIssue.RequireSha256(requestSha256); Code = code; ReasonCode = text; ExpectedCatalogRevision = expectedCatalogRevision; ExpectedGroupRevision = expectedGroupRevision; CatalogRevision = catalogRevision; GroupRevision = groupRevision; } internal bool Matches(StableIdentity actor, string requestSha256) { if (actor != null && Actor.Equals(actor)) { return string.Equals(RequestSha256, requestSha256, StringComparison.Ordinal); } return false; } } internal static class GroupCommandToken { internal static string Format(Guid epoch, long sequence) { if (epoch == Guid.Empty || sequence < 1) { throw new ArgumentException("Token identity is invalid."); } return epoch.ToString("N") + ":" + sequence.ToString("x16", CultureInfo.InvariantCulture); } internal static bool TryParse(string value, out Guid epoch, out long sequence) { epoch = Guid.Empty; sequence = 0L; if (value == null || value.Length != 49 || value[32] != ':' || !Guid.TryParseExact(value.Substring(0, 32), "N", out epoch) || epoch == Guid.Empty || !long.TryParse(value.Substring(33), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out sequence) || sequence < 1) { epoch = Guid.Empty; sequence = 0L; return false; } return string.Equals(value, Format(epoch, sequence), StringComparison.Ordinal); } } internal sealed class GroupCommandLedger { private readonly GroupCommandIssue[] _issues; private readonly GroupCommandReceipt[] _receipts; private readonly ReadOnlyCollection<GroupCommandIssue> _issueView; private readonly ReadOnlyCollection<GroupCommandReceipt> _receiptView; internal static GroupCommandLedger Empty { get; } = new GroupCommandLedger(Guid.Empty, 0L, 0L); internal Guid Epoch { get; } internal long NextSequence { get; } internal long MinimumAcceptedSequence { get; } internal IReadOnlyList<GroupCommandIssue> Issues => _issueView; internal IReadOnlyList<GroupCommandReceipt> Receipts => _receiptView; internal GroupCommandLedger(Guid epoch, long nextSequence, long minimumAcceptedSequence, IEnumerable<GroupCommandIssue> issues = null, IEnumerable<GroupCommandReceipt> receipts = null) { if (nextSequence < 0 || minimumAcceptedSequence < 0 || minimumAcceptedSequence > nextSequence || epoch == Guid.Empty != (nextSequence == 0)) { throw new ArgumentException("The Group command ledger frontier is invalid."); } Epoch = epoch; NextSequence = nextSequence; MinimumAcceptedSequence = minimumAcceptedSequence; _issues = SortedIssues(issues, epoch, minimumAcceptedSequence, nextSequence); _receipts = SortedReceipts(receipts, epoch, minimumAcceptedSequence, nextSequence); if (_issues.Length > 256 || _receipts.Length > 4096) { throw new ArgumentOutOfRangeException("issues"); } int num = 0; int num2 = 0; for (long num3 = minimumAcceptedSequence + 1; num3 <= nextSequence; num3++) { bool num4 = num < _issues.Length && _issues[num].Sequence == num3; bool flag = num2 < _receipts.Length && _receipts[num2].Sequence == num3; if (num4 == flag) { throw new ArgumentException("The Group command ledger has a gap or duplicate."); } if (num4) { num++; } else { num2++; } } _issueView = Array.AsReadOnly(_issues); _receiptView = Array.AsReadOnly(_receipts); } internal GroupCommandLedger Issue(StableIdentity actor, string requestSha256, Guid groupId, long expectedCatalogRevision, long expectedGroupRevision, long nowUtcTicks, long expiresUtcTicks, out GroupCommandIssue issue) { if (actor == null || nowUtcTicks <= 0 || expiresUtcTicks <= nowUtcTicks || expiresUtcTicks - nowUtcTicks > GroupCommandDurabilityLimits.MaximumIssueLifetime.Ticks) { throw new ArgumentException("The Group command issue lifetime is invalid."); } GroupCommandLedger groupCommandLedger = PruneExpired(nowUtcTicks); GroupCommandIssue[] issues = groupCommandLedger._issues; foreach (GroupCommandIssue groupCommandIssue in issues) { if (groupCommandIssue.Matches(actor, requestSha256) && groupCommandIssue.GroupId == groupId && groupCommandIssue.ExpectedCatalogRevision == expectedCatalogRevision && groupCommandIssue.ExpectedGroupRevision == expectedGroupRevision) { issue = groupCommandIssue; return groupCommandLedger; } } if (groupCommandLedger._issues.Length >= 256 || groupCommandLedger.NextSequence == long.MaxValue) { throw new InvalidOperationException("group-command-issue-capacity"); } Guid epoch = ((groupCommandLedger.Epoch == Guid.Empty) ? Guid.NewGuid() : groupCommandLedger.Epoch); long num = checked(groupCommandLedger.NextSequence + 1); issue = new GroupCommandIssue(epoch, num, actor, requestSha256, groupId, expectedCatalogRevision, expectedGroupRevision, expiresUtcTicks); List<GroupCommandIssue> issues2 = new List<GroupCommandIssue>(groupCommandLedger._issues) { issue }; return new GroupCommandLedger(epoch, num, groupCommandLedger.MinimumAcceptedSequence, issues2, groupCommandLedger._receipts); } internal GroupCommandLedger Complete(GroupCommandIssue issue, GroupMutationCode code, string reasonCode, long catalogRevision, long groupRevision, out GroupCommandReceipt receipt) { if (issue == null || issue.Epoch != Epoch || !TryGetIssue(issue.Token, out var issue2) || (issue2 != issue && !SameIssue(issue2, issue))) { throw new ArgumentException("The Group command issue is not current.", "issue"); } receipt = new GroupCommandReceipt(Epoch, issue.Sequence, issue.Actor, issue.RequestSha256, code, reasonCode, issue.ExpectedCatalogRevision, issue.ExpectedGroupRevision, catalogRevision, groupRevision); List<GroupCommandIssue> issues = _issues.Where((GroupCommandIssue value) => value.Sequence != issue.Sequence).ToList(); List<GroupCommandReceipt> list = new List<GroupCommandReceipt>(_receipts) { receipt }; list.Sort((GroupCommandReceipt left, GroupCommandReceipt right) => left.Sequence.CompareTo(right.Sequence)); return Compact(Epoch, NextSequence, MinimumAcceptedSequence, issues, list); } internal GroupCommandLedger PruneExpired(long nowUtcTicks) { if (nowUtcTicks <= 0) { throw new ArgumentOutOfRangeException("nowUtcTicks"); } List<GroupCommandIssue> list = new List<GroupCommandIssue>(_issues.Length); List<GroupCommandReceipt> list2 = new List<GroupCommandReceipt>(_receipts); bool flag = false; GroupCommandIssue[] issues = _issues; foreach (GroupCommandIssue groupCommandIssue in issues) { if (groupCommandIssue.ExpiresUtcTicks >= nowUtcTicks) { list.Add(groupCommandIssue); continue; } list2.Add(new GroupCommandReceipt(Epoch, groupCommandIssue.Sequence, groupCommandIssue.Actor, groupCommandIssue.RequestSha256, GroupMutationCode.RevisionConflict, "group-command-token-expired", groupCommandIssue.ExpectedCatalogRevision, groupCommandIssue.ExpectedGroupRevision, groupCommandIssue.ExpectedCatalogRevision, groupCommandIssue.ExpectedGroupRevision)); flag = true; } if (!flag) { return this; } list2.Sort((GroupCommandReceipt left, GroupCommandReceipt right) => left.Sequence.CompareTo(right.Sequence)); return Compact(Epoch, NextSequence, MinimumAcceptedSequence, list, list2); } internal bool TryGetIssue(string token, out GroupCommandIssue issue) { issue = null; if (!GroupCommandToken.TryParse(token, out var epoch, out var sequence) || epoch != Epoch || sequence <= MinimumAcceptedSequence || sequence > NextSequence) { return false; } for (int i = 0; i < _issues.Length; i++) { if (_issues[i].Sequence == sequence) { issue = _issues[i]; return true; } } return false; } internal bool TryGetReceipt(string token, out GroupCommandReceipt receipt) { receipt = null; if (!GroupCommandToken.TryParse(token, out var epoch, out var sequence) || epoch != Epoch || sequence <= MinimumAcceptedSequence || sequence > NextSequence) { return false; } for (int i = 0; i < _receipts.Length; i++) { if (_receipts[i].Sequence == sequence) { receipt = _receipts[i]; return true; } } return false; } private static GroupCommandLedger Compact(Guid epoch, long nextSequence, long minimum, List<GroupCommandIssue> issues, List<GroupCommandReceipt> receipts) { while (receipts.Count > 4096) { long next = minimum + 1; int num = receipts.FindIndex((GroupCommandReceipt value) => value.Sequence == next); if (num < 0) { throw new InvalidOperationException("group-command-receipt-capacity"); } receipts.RemoveAt(num); minimum = next; } return new GroupCommandLedger(epoch, nextSequence, minimum, issues, receipts); } private static GroupCommandIssue[] SortedIssues(IEnumerable<GroupCommandIssue> values, Guid epoch, long minimum, long maximum) { GroupCommandIssue[] array = (values ?? Array.Empty<GroupCommandIssue>()).OrderBy((GroupCommandIssue value) => value?.Sequence ?? 0).ToArray(); long num = minimum; GroupCommandIssue[] array2 = array; foreach (GroupCommandIssue groupCommandIssue in array2) { if (groupCommandIssue == null || groupCommandIssue.Epoch != epoch || groupCommandIssue.Sequence <= num || groupCommandIssue.Sequence > maximum) { throw new ArgumentException("The Group command issue set is invalid."); } num = groupCommandIssue.Sequence; } return array; } private static GroupCommandReceipt[] SortedReceipts(IEnumerable<GroupCommandReceipt> values, Guid epoch, long minimum, long maximum) { GroupCommandReceipt[] array = (values ?? Array.Empty<GroupCommandReceipt>()).OrderBy((GroupCommandReceipt value) => value?.Sequence ?? 0).ToArray(); long num = minimum; GroupCommandReceipt[] array2 = array; foreach (GroupCommandReceipt groupCommandReceipt in array2) { if (groupCommandReceipt == null || groupCommandReceipt.Epoch != epoch || groupCommandReceipt.Sequence <= num || groupCommandReceipt.Sequence > maximum) { throw new ArgumentException("The Group command receipt set is invalid."); } num = groupCommandReceipt.Sequence; } return array; } private static bool SameIssue(GroupCommandIssue left, GroupCommandIssue right) { if (left != null && right != null && left.Epoch == right.Epoch && left.Sequence == right.Sequence && left.Actor.Equals(right.Actor) && string.Equals(left.RequestSha256, right.RequestSha256, StringComparison.Ordinal) && left.GroupId == right.GroupId && left.ExpectedCatalogRevision == right.ExpectedCatalogRevision && left.ExpectedGroupRevision == right.ExpectedGroupRevision) { return left.ExpiresUtcTicks == right.ExpiresUtcTicks; } return false; } } public enum GroupMutationCode { Created, Renamed, Invited, InvitationCancelled, Accepted, Left, Removed, RoleChanged, OwnershipTransferred, Deleted, ExpiredInvitationsPruned, NoChange, RevisionConflict, InvalidRequest, GroupMissing, NameConflict, Unauthorized, AlreadyMember, InvitationMissing, InvitationExpired, CapacityReached, RetiredIdentity } public sealed class GroupMutationResult { public GroupMutationCode Code { get; } public string ReasonCode { get; } public GroupCatalog Catalog { get; } public GroupRecord Group { get; } public bool Success { get { if (Code >= GroupMutationCode.Created) { return Code <= GroupMutationCode.NoChange; } return false; } } internal GroupMutationResult(GroupMutationCode code, string reasonCode, GroupCatalog catalog, GroupRecord group) { Code = code; ReasonCode = reasonCode ?? throw new ArgumentNullException("reasonCode"); Catalog = catalog ?? throw new ArgumentNullException("catalog"); Group = group; } } public sealed class GroupMembership { public Guid GroupId { get; } public string GroupIdText { get; } public string DisplayName { get; } public GroupRole Role { get; } public long GroupRevision { get; } internal GroupMembership(Guid groupId, string displayName, GroupRole role, long groupRevision) { GroupId = groupId; GroupIdText = GroupIdentity.ToCanonicalId(groupId); DisplayName = displayName; Role = role; GroupRevision = groupRevision; } } public sealed class GroupCatalog { private readonly GroupRecord[] _groups; private readonly RetiredGroupId[] _retired; private readonly ReadOnlyCollection<GroupRecord> _groupView; private readonly ReadOnlyCollection<RetiredGroupId> _retiredView; public long Revision { get; } internal GroupCommandLedger CommandLedger { get; } public IReadOnlyList<GroupRecord> Groups => _groupView; public IReadOnlyList<RetiredGroupId> RetiredGroupIds => _retiredView; public static GroupCatalog Empty { get; } = new GroupCatalog(0L); public GroupCatalog(long revision, IEnumerable<GroupRecord> groups = null, IEnumerable<RetiredGroupId> retiredGroupIds = null) : this(revision, groups, retiredGroupIds, GroupCommandLedger.Empty) { } internal GroupCatalog(long revision, IEnumerable<GroupRecord> groups, IEnumerable<RetiredGroupId> retiredGroupIds, GroupCommandLedger commandLedger) { if (revision < 0) { throw new ArgumentOutOfRangeException("revision"); } Revision = revision; CommandLedger = commandLedger ?? throw new ArgumentNullException("commandLedger"); _groups = CopyGroups(groups, revision); _retired = CopyRetired(retiredGroupIds, revision, _groups); ValidateMembershipBounds(_groups); _groupView = Array.AsReadOnly(_groups); _retiredView = Array.AsReadOnly(_retired); } internal GroupCatalog WithCommandLedger(GroupCommandLedger ledger) { return new GroupCatalog(Revision, _groups, _retired, ledger); } public bool TryGetGroup(Guid id, out GroupRecord group) { group = null; if (id == Guid.Empty) { return false; } int num = FindGroup(_groups, GroupIdentity.ToCanonicalId(id)); if (num < 0) { return false; } group = _groups[num]; return true; } public IReadOnlyList<GroupMembership> GetMemberships(StableIdentity identity) { if (identity == null) { return Array.Empty<GroupMembership>(); } List<GroupMembership> list = new List<GroupMembership>(); GroupRecord[] groups = _groups; foreach (GroupRecord groupRecord in groups) { if (groupRecord.TryGetMember(identity, out var member)) { list.Add(new GroupMembership(groupRecord.Id, groupRecord.DisplayName, member.Role, groupRecord.Revision)); } } return list.AsReadOnly(); } public GroupMutationResult Create(long expectedCatalogRevision, Guid id, string displayName, StableIdentity owner) { if (!Expected(expectedCatalogRevision)) { return Conflict(); } if (id == Guid.Empty || owner == null || !TryDisplayName(displayName)) { return Invalid(); } string idText = GroupIdentity.ToCanonicalId(id); if (FindGroup(_groups, idText) >= 0) { return Fail(GroupMutationCode.InvalidRequest, "group-id-exists"); } if (FindRetired(_retired, idText) >= 0) { return Fail(GroupMutationCode.RetiredIdentity, "group-id-retired"); } if (_groups.Length >= 256 || CountMemberships(owner) >= 64) { return Fail(GroupMutationCode.CapacityReached, "group-capacity-reached"); } if (NameExists(displayName, null)) { return Fail(GroupMutationCode.NameConflict, "group-name-conflict"); } GroupRecord replacement = new GroupRecord(id, displayName, 1L, new GroupMember[1] { new GroupMember(owner, GroupRole.Owner, 1L) }); return Replace(null, replacement, GroupMutationCode.Created, "group-created"); } public GroupMutationResult Rename(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor, string displayName) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (!TryDisplayName(displayName) || actor == null) { return Invalid(group); } if (!IsOwner(group, actor)) { return Unauthorized(group); } if (NameExists(displayName, group.IdText)) { return Fail(GroupMutationCode.NameConflict, "group-name-conflict", group); } if (string.Equals(group.DisplayName, displayName, StringComparison.Ordinal)) { return Ok(GroupMutationCode.NoChange, "group-name-unchanged", group); } return Replace(group, group.Rename(displayName), GroupMutationCode.Renamed, "group-renamed"); } public GroupMutationResult Invite(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor, StableIdentity invitee, long nowUtcTicks, long expiresUtcTicks) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (actor == null || invitee == null || nowUtcTicks <= 0 || expiresUtcTicks <= nowUtcTicks || expiresUtcTicks - nowUtcTicks > GroupLimits.MaximumInvitationLifetime.Ticks) { return Invalid(group); } if (!IsOfficerOrOwner(group, actor)) { return Unauthorized(group); } if (group.TryGetMember(invitee, out var _)) { return Fail(GroupMutationCode.AlreadyMember, "group-already-member", group); } if (CountMemberships(invitee) >= 64) { return Fail(GroupMutationCode.CapacityReached, "group-membership-capacity", group); } if (group.TryGetInvitation(invitee, out var invitation)) { if (invitation.InvitedBy.Equals(actor) && invitation.ExpiresUtcTicks == expiresUtcTicks) { return Ok(GroupMutationCode.NoChange, "group-invitation-unchanged", group); } return Fail(GroupMutationCode.InvalidRequest, "group-invitation-exists", group); } if (group.Invitations.Count >= 256) { return Fail(GroupMutationCode.CapacityReached, "group-invitation-capacity", group); } return Replace(group, group.Invite(actor, invitee, expiresUtcTicks), GroupMutationCode.Invited, "group-invited"); } public GroupMutationResult CancelInvitation(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor, StableIdentity invitee) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (actor == null || invitee == null) { return Invalid(group); } if (!IsOfficerOrOwner(group, actor)) { return Unauthorized(group); } if (!group.TryGetInvitation(invitee, out var _)) { return Fail(GroupMutationCode.InvitationMissing, "group-invitation-missing", group); } return Replace(group, group.CancelInvitation(invitee), GroupMutationCode.InvitationCancelled, "group-invitation-cancelled"); } public GroupMutationResult Accept(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity invitee, long nowUtcTicks) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (invitee == null || nowUtcTicks <= 0) { return Invalid(group); } if (group.TryGetMember(invitee, out var _)) { return Fail(GroupMutationCode.AlreadyMember, "group-already-member", group); } if (!group.TryGetInvitation(invitee, out var invitation)) { return Fail(GroupMutationCode.InvitationMissing, "group-invitation-missing", group); } if (!invitation.Invitee.Equals(invitee)) { return Fail(GroupMutationCode.Unauthorized, "group-invitation-identity-mismatch", group); } if (invitation.IsExpired(nowUtcTicks)) { return Fail(GroupMutationCode.InvitationExpired, "group-invitation-expired", group); } if (group.Members.Count >= 256 || CountMemberships(invitee) >= 64) { return Fail(GroupMutationCode.CapacityReached, "group-membership-capacity", group); } return Replace(group, group.Accept(invitee), GroupMutationCode.Accepted, "group-invitation-accepted"); } public GroupMutationResult Leave(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (actor == null || !group.TryGetMember(actor, out var member)) { return Unauthorized(group); } if (member.Role == GroupRole.Owner) { return Fail(GroupMutationCode.Unauthorized, "group-owner-transfer-required", group); } return Replace(group, group.RemoveMember(actor), GroupMutationCode.Left, "group-left"); } public GroupMutationResult Remove(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor, StableIdentity target) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (actor == null || target == null || actor.Equals(target) || !group.TryGetMember(actor, out var member) || !group.TryGetMember(target, out var member2)) { return Unauthorized(group); } if (member2.Role == GroupRole.Owner || member.Role == GroupRole.Member || (member.Role == GroupRole.Officer && member2.Role != GroupRole.Member)) { return Unauthorized(group); } return Replace(group, group.RemoveMember(target), GroupMutationCode.Removed, "group-member-removed"); } public GroupMutationResult SetRole(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor, StableIdentity target, GroupRole role) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (actor == null || target == null || role == GroupRole.Owner || !Enum.IsDefined(typeof(GroupRole), role) || !IsOwner(group, actor) || !group.TryGetMember(target, out var member) || member.Role == GroupRole.Owner) { return Unauthorized(group); } if (member.Role == role) { return Ok(GroupMutationCode.NoChange, "group-role-unchanged", group); } return Replace(group, group.SetRole(target, role), GroupMutationCode.RoleChanged, "group-role-changed"); } public GroupMutationResult TransferOwnership(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor, StableIdentity successor) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (actor == null || successor == null || actor.Equals(successor) || !IsOwner(group, actor) || !group.TryGetMember(successor, out var member) || member.Role == GroupRole.Owner) { return Unauthorized(group); } return Replace(group, group.TransferOwnership(actor, successor), GroupMutationCode.OwnershipTransferred, "group-ownership-transferred"); } public GroupMutationResult Delete(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (actor == null || !IsOwner(group, actor)) { return Unauthorized(group); } if (_retired.Length >= 4096) { return Fail(GroupMutationCode.CapacityReached, "group-retired-id-capacity", group); } long num = checked(Revision + 1); GroupRecord[] groups = _groups.Where((GroupRecord value) => value.Id != id).ToArray(); List<RetiredGroupId> retiredGroupIds = new List<RetiredGroupId>(_retired) { new RetiredGroupId(id, num) }; GroupCatalog catalog = new GroupCatalog(num, groups, retiredGroupIds, CommandLedger); return new GroupMutationResult(GroupMutationCode.Deleted, "group-deleted", catalog, null); } public GroupMutationResult PruneExpiredInvitations(long expectedCatalogRevision, Guid id, long expectedGroupRevision, long nowUtcTicks) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (nowUtcTicks <= 0) { return Invalid(group); } GroupRecord groupRecord = group.PruneExpired(nowUtcTicks); if (group != groupRecord) { return Replace(group, groupRecord, GroupMutationCode.ExpiredInvitationsPruned, "group-expired-invitations-pruned"); } return Ok(GroupMutationCode.NoChange, "group-no-expired-invitations", group); } private bool TryMutation(long expectedCatalogRevision, Guid id, long expectedGroupRevision, out GroupRecord group, out GroupMutationResult failure) { group = null; failure = null; if (!Expected(expectedCatalogRevision)) { failure = Conflict(); return false; } if (id == Guid.Empty || expectedGroupRevision < 1) { failure = Invalid(); return false; } if (!TryGetGroup(id, out group)) { failure = Fail(GroupMutationCode.GroupMissing, "group-missing"); return false; } if (group.Revision != expectedGroupRevision) { failure = Fail(GroupMutationCode.RevisionConflict, "group-revision-conflict", group); return false; } return true; } private GroupMutationResult Replace(GroupRecord current, GroupRecord replacement, GroupMutationCode code, string reason) { List<GroupRecord> list = new List<GroupRecord>(_groups.Length + ((current == null) ? 1 : 0)); GroupRecord[] groups = _groups; foreach (GroupRecord groupRecord in groups) { if (current == null || groupRecord.Id != current.Id) { list.Add(groupRecord); } } list.Add(replacement); GroupCatalog catalog = new GroupCatalog(checked(Revision + 1), list, _retired, CommandLedger); return new GroupMutationResult(code, reason, catalog, replacement); } private bool NameExists(string name, string exceptId) { GroupRecord[] groups = _groups; foreach (GroupRecord groupRecord in groups) { if (!string.Equals(groupRecord.IdText, exceptId, StringComparison.Ordinal) && string.Equals(groupRecord.DisplayName, name, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private int CountMemberships(StableIdentity identity) { int num = 0; GroupRecord[] groups = _groups; for (int i = 0; i < groups.Length; i++) { if (groups[i].TryGetMember(identity, out var _)) { num++; } } return num; } private static bool IsOwner(GroupRecord group, StableIdentity identity) { if (group.TryGetMember(identity, out var member)) { return member.Role == GroupRole.Owner; } return false; } private static bool IsOfficerOrOwner(GroupRecord group, StableIdentity identity) { if (group.TryGetMember(identity, out var member)) { return (int)member.Role >= 2; } return false; } private bool Expected(long revision) { if (revision >= 0) { return revision == Revision; } return false; } private GroupMutationResult Conflict() { return Fail(GroupMutationCode.RevisionConflict, "group-catalog-revision-conflict"); } private GroupMutationResult Invalid(GroupRecord group = null) { return Fail(GroupMutationCode.InvalidRequest, "group-request-invalid", group); } private GroupMutationResult Unauthorized(GroupRecord group) { return Fail(GroupMutationCode.Unauthorized, "group-actor-unauthorized", group); } private GroupMutationResult Ok(GroupMutationCode code, string reason, GroupRecord group) { return new GroupMutationResult(code, reason, this, group); } private GroupMutationResult Fail(GroupMutationCode code, string reason, GroupRecord group = null) { return new GroupMutationResult(code, reason, this, group); } private static bool TryDisplayName(string value) { try { GroupIdentity.RequireDisplayName(value); return true; } catch (ArgumentException) { return false; } } private static GroupRecord[] CopyGroups(IEnumerable<GroupRecord> source, long revision) { GroupRecord[] array = (source ?? Array.Empty<GroupRecord>()).ToArray(); if (array.Length > 256 || array.Any((GroupRecord value) => value == null || value.Revision > revision)) { throw new ArgumentOutOfRangeException("source"); } Array.Sort(array, (GroupRecord left, GroupRecord right) => string.Compare(left.IdText, right.IdText, StringComparison.Ordinal)); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); for (int num = 0; num < array.Length; num++) { if (num > 0 && string.Equals(array[num - 1].IdText, array[num].IdText, StringComparison.Ordinal)) { throw new ArgumentException("Group UUIDs must be unique.", "source"); } if (!hashSet.Add(array[num].DisplayName)) { throw new ArgumentException("Group display names must be unique.", "source"); } } return array; } private static RetiredGroupId[] CopyRetired(IEnumerable<RetiredGroupId> source, long revision, IReadOnlyList<GroupRecord> groups) { RetiredGroupId[] array = (source ?? Array.Empty<RetiredGroupId>()).ToArray(); if (array.Length > 4096 || array.Any((RetiredGroupId value) => value == null || value.DeletedCatalogRevision > revision)) { throw new ArgumentOutOfRangeException("source"); } Array.Sort(array, (RetiredGroupId left, RetiredGroupId right) => string.Compare(left.IdText, right.IdText, StringComparison.Ordinal)); for (int num = 0; num < array.Length; num++) { if (num > 0 && string.Equals(array[num - 1].IdText, array[num].IdText, StringComparison.Ordinal)) { throw new ArgumentException("Retired group UUIDs must be unique.", "source"); } if (FindGroup(groups, array[num].IdText) >= 0) { throw new ArgumentException("A current group UUID cannot also be retired.", "source"); } } return array; } private static void ValidateMembershipBounds(IEnumerable<GroupRecord> groups) { Dictionary<StableIdentity, int> dictionary = new Dictionary<StableIdentity, int>(); foreach (GroupRecord group in groups) { foreach (GroupMember member in group.Members) { dictionary.TryGetValue(member.Identity, out var value); value++; if (value > 64) { throw new ArgumentOutOfRangeException("groups"); } dictionary[member.Identity] = value; } } } private static int FindGroup(IReadOnlyList<GroupRecord> groups, string idText) { int num = 0; int num2 = groups.Count - 1; while (num <= num2) { int num3 = num + (num2 - num) / 2; int num4 = string.Compare(groups[num3].IdText, idText, StringComparison.Ordinal); if (num4 == 0) { return num3; } if (num4 < 0) { num = num3 + 1; } else { num2 = num3 - 1; } } return -1; } private static int FindRetired(IReadOnlyList<RetiredGroupId> retired, string idText) { int num = 0; int num2 = retired.Count - 1; while (num <= num2) { int num3 = num + (num2 - num) / 2; int num4 = string.Compare(retired[num3].IdText, idText, StringComparison.Ordinal); if (num4 == 0) { return num3; } if (num4 < 0) { num = num3 + 1; } else { num2 = num3 - 1; } } return -1; } } internal static class GroupCatalogCodec { private const uint Magic = 1196446279u; private const ushort SchemaVersion = 2; private const int DigestBytes = 32; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); internal static byte[] Encode(string worldScope, GroupCatalog catalog) { worldScope = GroupIdentity.RequireWorldScope(worldScope); if (catalog == null) { throw new ArgumentNullException("catalog"); } byte[] array; using (MemoryStream memoryStream = new MemoryStream()) { using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, StrictUtf8, leaveOpen: true); binaryWriter.Write(1196446279u); binaryWriter.Write((ushort)2); WriteText(binaryWriter, worldScope, 128); binaryWriter.Write(catalog.Revision); binaryWriter.Write(catalog.Groups.Count); foreach (GroupRecord group in catalog.Groups) { WriteText(binaryWriter, group.IdText, 32); WriteText(binaryWriter, group.DisplayName, 64); binaryWriter.Write(group.Revision); binaryWriter.Write(group.Members.Count); foreach (GroupMember member in group.Members) { WriteIdentity(binaryWriter, member.Identity); binaryWriter.Write((byte)member.Role); binaryWriter.Write(member.JoinedRevision); } binaryWriter.Write(group.Invitations.Count); foreach (GroupInvitation invitation in group.Invitations) { WriteIdentity(binaryWriter, invitation.Invitee); WriteIdentity(binaryWriter, invitation.InvitedBy); binaryWriter.Write(invitation.IssuedRevision); binaryWriter.Write(invitation.ExpiresUtcTicks); } } binaryWriter.Write(catalog.RetiredGroupIds.Count); foreach (RetiredGroupId retiredGroupId in catalog.RetiredGroupIds) { WriteText(binaryWriter, retiredGroupId.IdText, 32); binaryWriter.Write(retiredGroupId.DeletedCatalogRevision); } WriteCommandLedger(binaryWriter, catalog.CommandLedger); binaryWriter.Flush(); array = memoryStream.ToArray(); } byte[] array2 = Sha256(array); if (array.Length > 8388608 - array2.Length) { throw new InvalidOperationException("The group catalog exceeds its hard byte limit."); } byte[] array3 = new byte[array.Length + array2.Length]; Buffer.BlockCopy(array, 0, array3, 0, array.Length); Buffer.BlockCopy(array2, 0, array3, array.Length, array2.Length); return array3; } internal static bool TryDecode(byte[] bytes, string expectedWorldScope, out GroupCatalog catalog, out string reason) { catalog = null; reason = "group-catalog-corrupt"; try { expectedWorldScope = GroupIdentity.RequireWorldScope(expectedWorldScope); if (bytes == null || bytes.Length < 55 || bytes.Length > 8388608) { return false; } int num = bytes.Length - 32; if (!FixedEquals(Sha256(bytes, 0, num), bytes, num)) { reason = "group-catalog-digest-mismatch"; return false; } using (MemoryStream memoryStream = new MemoryStream(bytes, 0, num, writable: false, publiclyVisible: true)) { using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8, leaveOpen: true); if (binaryReader.ReadUInt32() != 1196446279) { reason = "group-catalog-schema-unsupported"; return false; } ushort num2 = binaryReader.ReadUInt16(); if (num2 != 1 && num2 != 2) { reason = "group-catalog-schema-unsupported"; return false; } if (!string.Equals(ReadText(binaryReader, 128), expectedWorldScope, StringComparison.Ordinal)) { reason = "group-catalog-world-mismatch"; return false; } long revision = binaryReader.ReadInt64(); int num3 = ReadCount(binaryReader, 256); List<GroupRecord> list = new List<GroupRecord>(num3); for (int i = 0; i < num3; i++) { if (!GroupIdentity.TryParseCanonicalId(ReadText(binaryReader, 32), out var groupId)) { return false; } string displayName = ReadText(binaryReader, 64); long revision2 = binaryReader.ReadInt64(); int num4 = ReadCount(binaryReader, 256); if (num4 < 1) { return false; } List<GroupMember> list2 = new List<GroupMember>(num4); for (int j = 0; j < num4; j++) { list2.Add(new GroupMember(ReadIdentity(binaryReader), (GroupRole)binaryReader.ReadByte(), binaryReader.ReadInt64())); } int num5 = ReadCount(binaryReader, 256); List<GroupInvitation> list3 = new List<GroupInvitation>(num5); for (int k = 0; k < num5; k++) { list3.Add(new GroupInvitation(ReadIdentity(binaryReader), ReadIdentity(binaryReader), binaryReader.ReadInt64(), binaryReader.ReadInt64())); } list.Add(new GroupRecord(groupId, displayName, revision2, list2, list3)); } int num6 = ReadCount(binaryReader, 4096); List<RetiredGroupId> list4 = new List<RetiredGroupId>(num6); for (int l = 0; l < num6; l++) { if (!GroupIdentity.TryParseCanonicalId(ReadText(binaryReader, 32), out var groupId2)) { return false; } list4.Add(new RetiredGroupId(groupId2, binaryReader.ReadInt64())); } GroupCommandLedger commandLedger = ((num2 == 1) ? GroupCommandLedger.Empty : ReadCommandLedger(binaryReader)); if (memoryStream.Position != num) { reason = "group-catalog-trailing-data"; return false; } catalog = new GroupCatalog(revision, list, list4, commandLedger); byte[] array = ((num2 == 2) ? Encode(expectedWorldScope, catalog) : null); if (array != null && !ExactEquals(bytes, array)) { catalog = null; reason = "group-catalog-noncanonical"; return false; } } reason = "group-catalog-ready"; return true; } catch (Exception ex) when (ex is ArgumentException || ex is IOException || ex is EndOfStreamException || ex is DecoderFallbackException || ex is OverflowException) { catalog = null; return false; } } internal static string ComputeSha256(byte[] bytes) { byte[] array = Sha256(bytes ?? Array.Empty<byte>()); StringBuilder stringBuilder = new StringBuilder(array.Length * 2); byte[] array2 = array; foreach (byte b in array2) { stringBuilder.Append(b.ToString("x2")); } return stringBuilder.ToString(); } private static void WriteIdentity(BinaryWriter writer, StableIdentity identity) { if (identity == null) { throw new ArgumentNullException("identity"); } WriteText(writer, identity.Authority, 64); WriteText(writer, identity.SubjectId, 1024); } private static StableIdentity ReadIdentity(BinaryReader reader) { return new StableIdentity(ReadText(reader, 64), ReadText(reader, 1024)); } private static void WriteCommandLedger(BinaryWriter writer, GroupCommandLedger ledger) { ledger = ledger ?? GroupCommandLedger.Empty; writer.Write(ledger.Epoch.ToByteArray()); writer.Write(ledger.NextSequence); writer.Write(ledger.MinimumAcceptedSequence); writer.Write(ledger.Issues.Count); foreach (GroupCommandIssue issue in ledger.Issues) { writer.Write(issue.Sequence); WriteIdentity(writer, issue.Actor); WriteText(writer, issue.RequestSha256, 64); WriteText(writer, GroupIdentity.ToCanonicalId(issue.GroupId), 32); writer.Write(issue.ExpectedCatalogRevision); writer.Write(issue.ExpectedGroupRevision); writer.Write(issue.ExpiresUtcTicks); } writer.Write(ledger.Receipts.Count); foreach (GroupCommandReceipt receipt in ledger.Receipts) { writer.Write(receipt.Sequence); WriteIdentity(writer, receipt.Actor); WriteText(writer, receipt.RequestSha256, 64); writer.Write((int)receipt.Code); WriteText(writer, receipt.ReasonCode, 96); writer.Write(receipt.ExpectedCatalogRevision); writer.Write(receipt.ExpectedGroupRevision); writer.Write(receipt.CatalogRevision); writer.Write(receipt.GroupRevision); } } private static GroupCommandLedger ReadCommandLedger(BinaryReader reader) { byte[] array = reader.ReadBytes(16); if (array.Length != 16) { throw new EndOfStreamException(); } Guid epoch = new Guid(array); long nextSequence = reader.ReadInt64(); long minimumAcceptedSequence = reader.ReadInt64(); int num = ReadCount(reader, 256); List<GroupCommandIssue> list = new List<GroupCommandIssue>(num); for (int i = 0; i < num; i++) { long sequence = reader.ReadInt64(); StableIdentity actor = ReadIdentity(reader); string requestSha = ReadText(reader, 64); if (!GroupIdentity.TryParseCanonicalId(ReadText(reader, 32), out var groupId)) { throw new InvalidDataException(); } list.Add(new GroupCommandIssue(epoch, sequence, actor, requestSha, groupId, reader.ReadInt64(), reader.ReadInt64(), reader.ReadInt64())); } int num2 = ReadCount(reader, 4096); List<GroupCommandReceipt> list2 = new List<GroupCommandReceipt>(num2); for (int j = 0; j < num2; j++) { list2.Add(new GroupCommandReceipt(epoch, reader.ReadInt64(), ReadIdentity(reader), ReadText(reader, 64), (GroupMutationCode)reader.ReadInt32(), ReadText(reader, 96), reader.ReadInt64(), reader.ReadInt64(), reader.ReadInt64(), reader.ReadInt64())); } return new GroupCommandLedger(epoch, nextSequence, minimumAcceptedSequence, list, list2); } private static void WriteText(BinaryWriter writer, string value, int maximumBytes) { byte[] bytes = StrictUtf8.GetBytes(value ?? string.Empty); if (bytes.Length < 1 || bytes.Length > maximumBytes) { throw new ArgumentOutOfRangeException("value"); } writer.Write(bytes.Length); writer.Write(bytes); } private static string ReadText(BinaryReader reader, int maximumBytes) { int num = reader.ReadInt32(); if (num < 1 || num > maximumBytes) { throw new InvalidDataException(); } byte[] array = reader.ReadBytes(num); if (array.Length != num) { throw new EndOfStreamException(); } return StrictUtf8.GetString(array); } private static int ReadCount(BinaryReader reader, int maximum) { int num = reader.ReadInt32(); if (num < 0 || num > maximum) { throw new InvalidDataException(); } return num; } private static byte[] Sha256(byte[] bytes) { return Sha256(bytes, 0, bytes.Length); } private static byte[] Sha256(byte[] bytes, int offset, int count) { using SHA256 sHA = SHA256.Create(); return sHA.ComputeHash(bytes, offset, count); } private static bool FixedEquals(byte[] expected, byte[] source, int offset) { if (expected.Length != source.Length - offset) { return false; } int num = 0; for (int i = 0; i < expected.Length; i++) { num |= expected[i] ^ source[offset + i]; } return num == 0; } private static bool ExactEquals(byte[] left, byte[] right) { if (left == null || right == null || left.Length != right.Length) { return false; } int num = 0; for (int i = 0; i < left.Length; i++) { num |= left[i] ^ right[i]; } return num == 0; } } public enum GroupCommandKind : byte { Create = 1, Rename, Invite, CancelInvitation, Accept, Leave, Remove, SetRole, TransferOwnership, Delete } public sealed class GroupCommand { public GroupCommandKind Kind { get; } public Guid GroupId { get; } public string DisplayName { get; } public StableIdentity Target { get; } public GroupRole Role { get; } public long InvitationExpiresUtcTicks { get; } public GroupCommand(GroupCommandKind kind, Guid groupId, string displayName = "", StableIdentity target = null, GroupRole role = GroupRole.Member, long invitationExpiresUtcTicks = 0L) { Kind = kind; GroupId = groupId; DisplayName = displayName ?? string.Empty; Target = target; Role = role; InvitationExpiresUtcTicks = invitationExpiresUtcTicks; Validate(); } private void Validate() { if (!Enum.IsDefined(typeof(GroupCommandKind), Kind) || GroupId == Guid.Empty) { throw new ArgumentException("The group command kind or ID is invalid."); } if (!Enum.IsDefined(typeof(GroupRole), Role)) { throw new ArgumentOutOfRangeException("Role"); } bool num = Kind == GroupCommandKind.Create || Kind == GroupCommandKind.Rename; bool flag = Kind == GroupCommandKind.Invite || Kind == GroupCommandKind.CancelInvitation || Kind == GroupCommandKind.Remove || Kind == GroupCommandKind.SetRole || Kind == GroupCommandKind.TransferOwnership; if (num) { GroupIdentity.RequireDisplayName(DisplayName); } else if (DisplayName.Length != 0) { throw new ArgumentException("This group command does not accept a display name."); } if (flag != (Target != null)) { throw new ArgumentException("The group command target shape is invalid."); } if (Kind == GroupCommandKind.SetRole) { if (Role != GroupRole.Member && Role != GroupRole.Officer) { throw new ArgumentException("SetRole accepts Member or Officer only."); } } else if (Role != GroupRole.Member) { throw new ArgumentException("This group command does not accept a role."); } if (Kind == GroupCommandKind.Invite) { if (InvitationExpiresUtcTicks <= 0) { throw new ArgumentOutOfRangeException("InvitationExpiresUtcTicks"); } } else if (InvitationExpiresUtcTicks != 0L) { throw new ArgumentException("This group command does not accept an expiry."); } } } public static class GroupCommandCodec { private const uint Magic = 1129337415u; private const byte Schema = 1; public const int MaximumPayloadBytes = 1024; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); public static byte[] Encode(GroupCommand command) { if (command == null) { throw new ArgumentNullException("command"); } using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, StrictUtf8, leaveOpen: true); binaryWriter.Write(1129337415u); binaryWriter.Write((byte)1); binaryWriter.Write((byte)command.Kind); binaryWriter.Write(command.GroupId.ToByteArray()); WriteText(binaryWriter, command.DisplayName, 64); WriteText(binaryWriter, command.Target?.Authority ?? string.Empty, 64); WriteText(binaryWriter, command.Target?.SubjectId ?? string.Empty, 1024); binaryWriter.Write((byte)command.Role); binaryWriter.Write(command.InvitationExpiresUtcTicks); binaryWriter.Flush(); if (memoryStream.Length > 1024) { throw new InvalidOperationException("The group command exceeds its wire bound."); } return memoryStream.ToArray(); } public static bool TryDecode(byte[] payload, out GroupCommand command, out string failureCode) { command = null; failureCode = "group-command-invalid"; if (payload == null || payload.Length < 33 || payload.Length > 1024) { return false; } try { using (MemoryStream memoryStream = new MemoryStream(payload, writable: false)) { using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8, leaveOpen: true); if (binaryReader.ReadUInt32() != 1129337415 || binaryReader.ReadByte() != 1) { failureCode = "group-command-schema-invalid"; return false; } GroupCommandKind kind = (GroupCommandKind)binaryReader.ReadByte(); byte[] array = binaryReader.ReadBytes(16); if (array.Length != 16) { failureCode = "group-command-id-truncated"; return false; } Guid groupId = new Guid(array); string displayName = ReadText(binaryReader, 64); string text = ReadText(binaryReader, 64); string text2 = ReadText(binaryReader, 1024); GroupRole role = (GroupRole)binaryReader.ReadByte(); long invitationExpiresUtcTicks = binaryReader.ReadInt64(); if (memoryStream.Position != memoryStream.Length) { failureCode = "group-command-trailing-data"; return false; } StableIdentity target = ((text.Length == 0 && text2.Length == 0) ? null : new StableIdentity(text, text2)); if (text.Length == 0 != (text2.Length == 0)) { failureCode = "group-command-target-invalid"; return false; } command = new GroupCommand(kind, groupId, displayName, target, role, invitationExpiresUtcTicks); } if (!payload.SequenceEqual(Encode(command))) { command = null; failureCode = "group-command-noncanonical"; return false; } failureCode = "ok"; return true; } catch (Exception ex) when (ex is ArgumentException || ex is EndOfStreamException || ex is IOException || ex is DecoderFallbackException || ex is OverflowException) { command = null; failureCode = "group-command-invalid"; return false; } } private static void WriteText(BinaryWriter writer, string value, int maximumBytes) { byte[] bytes = StrictUtf8.GetBytes(value ?? string.Empty); if (bytes.Length > maximumBytes || bytes.Length > 65535) { throw new ArgumentOutOfRangeException("value"); } writer.Write((ushort)bytes.Length); writer.Write(bytes); } private static string ReadText(BinaryReader reader, int maximumBytes) { int num = reader.ReadUInt16(); if (num > maximumBytes || num > reader.BaseStream.Length - reader.BaseStream.Position) { throw new InvalidDataException("The group command text length is invalid."); } byte[] array = reader.ReadBytes(num); if (array.Length != num) { throw new EndOfStreamException(); } string text = StrictUtf8.GetString(array); if (!StrictUtf8.GetBytes(text).SequenceEqual(array)) { throw new InvalidDataException("The group command text is not canonical UTF-8."); } return text; } } public sealed class GroupCommandExecutionResult { public GroupMutationCode Code { get; } public string ReasonCode { get; } public GroupCatalog Catalog { get; } public GroupRecord Group { get; } public bool Success { get { if (Code >= GroupMutationCode.Created) { return Code <= GroupMutationCode.NoChange; } return false; } } internal GroupCommandExecutionResult(GroupMutationCode code, string reasonCode, GroupCatalog catalog, GroupRecord group) { Code = code; ReasonCode = reasonCode ?? string.Empty; Catalog = catalog; Group = group; } } public sealed class GroupCommandProcessor { private readonly IGroupWorldStore _store; private readonly Func<string> _worldScopeProvider; private readonly Func<long> _utcTicksProvider; public GroupCommandProcessor(IGroupWorldStore store, Func<string> worldScopeProvider, Func<long> utcTicksProvider = null) { _store = store ?? throw new ArgumentNullException("store"); _worldScopeProvider = worldScopeProvider ?? throw new ArgumentNullException("worldScopeProvider"); _utcTicksProvider = utcTicksProvider ?? ((Func<long>)(() => DateTime.UtcNow.Ticks)); } public GroupCommandExecutionResult Execute(StableIdentity actor, GroupCommand command) { if (actor == null || command == null) { return Fail(GroupMutationCode.InvalidRequest, "group-command-invalid", null); } string text; try { text = _worldScopeProvider() ?? string.Empty; } catch { return Fail(GroupMutationCode.RevisionConflict, "group-world-unavailable", null); } if (text.Length == 0) { return Fail(GroupMutationCode.RevisionConflict, "group-world-unavailable", null); } GroupWorldReadResult groupWorldReadResult; try { groupWorldReadResult = _store.Read(text); } catch { return Fail(GroupMutationCode.RevisionConflict, "group-store-unavailable", null); } if (groupWorldReadResult == null || groupWorldReadResult.State == GroupWorldReadState.Corrupt || groupWorldReadResult.State == GroupWorldReadState.EvidenceConflict) { return Fail(GroupMutationCode.RevisionConflict, "group-store-evidence-conflict", groupWorldReadResult?.Catalog); } if (groupWorldReadResult.State == GroupWorldReadState.Unavailable) { return Fail(GroupMutationCode.RevisionConflict, "group-store-unavailable", groupWorldReadResult.Catalog); } GroupCatalog groupCatalog = ((groupWorldReadResult.State == GroupWorldReadState.Missing) ? GroupCatalog.Empty : groupWorldReadResult.Catalog); if (groupCatalog == null) { return Fail(GroupMutationCode.RevisionConflict, "group-store-invalid", null); } GroupCommandExecutionResult groupCommandExecutionResult = TryExactDesiredReplay(groupCatalog, actor, command); if (groupCommandExecutionResult != null) { return groupCommandExecutionResult; } GroupMutationResult groupMutationResult; if (command.Kind == GroupCommandKind.Create) { groupMutationResult = groupCatalog.Create(groupCatalog.Revision, command.GroupId, command.DisplayName, actor); } else { if (!groupCatalog.TryGetGroup(command.GroupId, out var group)) { return Fail(GroupMutationCode.GroupMissing, "group-missing", groupCatalog); } long now = _utcTicksProvider(); groupMutationResult = Apply(groupCatalog, group, actor, command, now); } if (!groupMutationResult.Success) { return From(groupMutationResult); } if (groupMutationResult.Code == GroupMutationCode.NoChange || groupMutationResult.Catalog == groupCatalog) { return From(groupMutationResult); } GroupWorldCommitResult groupWorldCommitResult; try { groupWorldCommitResult = _store.TryCommit(text, groupCatalog.Revision, groupMutationResult.Catalog); } catch { return Fail(GroupMutationCode.RevisionConflict, "group-store-commit-failed", groupCatalog); } if (groupWorldCommitResult == null || !groupWorldCommitResult.Success) { return Fail(GroupMutationCode.RevisionConflict, groupWorldCommitResult?.ReasonCode ?? "group-store-commit-failed", groupWorldCommitResult?.Current?.Catalog ?? groupCatalog); } return From(groupMutationResult); } internal static GroupCommandExecutionResult EvaluateIssued(GroupCatalog current, StableIdentity actor, GroupCommand command, long expectedCatalogRevision, long expectedGroupRevision, long nowUtcTicks) { if (current == null || actor == null || command == null || nowUtcTicks <= 0) { return Fail(GroupMutationCode.InvalidRequest, "group-command-invalid", current); } if (current.Revision != expectedCatalogRevision) { return Fail(GroupMutationCode.RevisionConflict, "group-catalog-revision-conflict", current); } GroupMutationResult result; if (command.Kind == GroupCommandKind.Create) { if (expectedGroupRevision != -1 || current.TryGetGroup(command.GroupId, out var _)) { return Fail(GroupMutationCode.RevisionConflict, "group-create-revision-conflict", current); } result = current.Create(expectedCatalogRevision, command.GroupId, command.DisplayName, actor); } else { if (expectedGroupRevision < 1 || !current.TryGetGroup(command.GroupId, out var group2) || group2.Revision != expectedGroupRevision) { return Fail(GroupMutationCode.RevisionConflict, "group-revision-conflict", current); } result = Apply(current, group2, actor, command, nowUtcTicks); } return From(result); } private static GroupMutationResult Apply(GroupCatalog catalog, GroupRecord group, StableIdentity actor, GroupCommand command, long now) { return command.Kind switch { GroupCommandKind.Rename => catalog.Rename(catalog.Revision, group.Id, group.Revision, actor, command.DisplayName), GroupCommandKind.Invite => catalog.Invite(catalog.Revision, group.Id, group.Revision, actor, command.Target, now, command.InvitationExpiresUtcTicks), GroupCommandKind.CancelInvitation => catalog.CancelInvitation(catalog.Revision, group.Id, group.Revision, actor, command.Target), GroupCommandKind.Accept => catalog.Accept(catalog.Revision, group.Id, group.Revision, actor, now), GroupCommandKind.Leave => catalog.Leave(catalog.Revision, group.Id, group.Revision, actor), GroupCommandKind.Remove => catalog.Remove(catalog.Revision, group.Id, group.Revision, actor, command.Target), GroupCommandKind.SetRole => catalog.SetRole(catalog.Revision, group.Id, group.Revision, actor, command.Target, command.Role), GroupCommandKind.TransferOwnership => catalog.TransferOwnership(catalog.Revision, group.Id, group.Revision, actor, command.Target), GroupCommandKind.Delete => catalog.Delete(catalog.Revision, group.Id, group.Revision, actor), _ => new GroupMutationResult(GroupMutationCode.InvalidRequest, "group-command-kind-invalid", catalog, group), }; } private static GroupCommandExecutionResult TryExactDesiredReplay(GroupCatalog catalog, StableIdentity actor, GroupCommand command) { if (command.Kind == GroupCommandKind.Create && catalog.TryGetGroup(command.GroupId, out var group) && string.Equals(group.DisplayName, command.DisplayName, StringComparison.Ordinal) && group.TryGetMember(actor, out var member) && member.Role == GroupRole.Owner) { return NoChange("group-create-already-applied", catalog, group); } if (command.Kind == GroupCommandKind.Delete && catalog.RetiredGroupIds.Any((RetiredGroupId value) => value.Id == command.GroupId)) { return NoChange("group-delete-already-applied", catalog, null); } if (!catalog.TryGetGroup(command.GroupId, out var group2)) { return null; } if (command.Kind == GroupCommandKind.Accept && group2.TryGetMember(actor, out var member2)) { return NoChange("group-accept-already-applied", catalog, group2); } if (command.Kind == GroupCommandKind.Leave && !group2.TryGetMember(actor, out member2)) { return NoChange("group-leave-already-applied", catalog, group2); } if (command.Kind == GroupCommandKind.TransferOwnership && command.Target != null && group2.TryGetMember(command.Target, out var member3) && member3.Role == GroupRole.Owner && group2.TryGetMember(actor, out member2)) { return NoChange("group-transfer-already-applied", catalog, group2); } return null; } private static GroupCommandExecutionResult From(GroupMutationResult result) { return new GroupCommandExecutionResult(result.Code, result.ReasonCode, result.Catalog, result.Group); } private static GroupCommandExecutionResult NoChange(string reason, GroupCatalog catalog, GroupRecord group) { return new GroupCommandExecutionResult(GroupMutationCode.NoChange, reason, catalog, group); } private static GroupCommandExecutionResult Fail(GroupMutationCode code, string reason, GroupCatalog catalog) { return new GroupCommandExecutionResult(code, reason, catalog, null); } } public enum ActiveGroupSelectionStatus : byte { Available = 1, NoneSelected, Stale, Ambiguous, GroupMissing, NotMember } public sealed class ActiveGroupSelection { public ActiveGroupSelectionStatus Status { get; } public string GroupId { get; } public string DisplayName { get; } public bool IsAvailable => Status == ActiveGroupSelectionStatus.Available; internal static ActiveGroupSelection None { get; } = new ActiveGroupSelection(ActiveGroupSelectionStatus.NoneSelected); internal static ActiveGroupSelection Stale { get; } = new ActiveGroupSelection(ActiveGroupSelectionStatus.Stale); internal static ActiveGroupSelection Ambiguous { get; } = new ActiveGroupSelection(ActiveGroupSelectionStatus.Ambiguous); internal ActiveGroupSelection(ActiveGroupSelectionStatus status, string groupId = "", string displayName = "") { if (!Enum.IsDefined(typeof(ActiveGroupSelectionStatus), status)) { throw new ArgumentOutOfRangeException("status"); } if (status == ActiveGroupSelectionStatus.Available && !GroupIdentity.IsCanonicalId(groupId)) { throw new ArgumentException("An available active Group requires an exact Group UUID.", "groupId"); } if (status != ActiveGroupSelectionStatus.Available && !string.IsNullOrEmpty(groupId)) { throw new ArgumentException("An unavailable active Group cannot expose an ID.", "groupId"); } Status = status; GroupId = groupId ?? string.Empty; DisplayName = displayName ?? string.Empty; } } public interface IActiveGroupSelectionService { ActiveGroupSelection Resolve(StableIdentity identity); } public enum GroupActiveSelectionReadState { Missing, Ready, Corrupt, Ambiguous, Unavailable } public sealed class GroupActiveSelectionReadResult { public GroupActiveSelectionReadState State { get; } public string ReasonCode { get; } public Guid GroupId { get; } public bool HasSelection { get { if (State == GroupActiveSelectionReadState.Ready) { return GroupId != Guid.Empty; } return false; } } internal GroupActiveSelectionReadResult(GroupActiveSelectionReadState state, string reasonCode, Guid groupId) { State = state; ReasonCode = reasonCode ?? string.Empty; GroupId = groupId; } } public interface IGroupActiveSelectionStore { GroupActiveSelectionReadResult Read(string worldScope, StableIdentity identity); bool TrySet(string worldScope, StableIdentity identity, Guid groupId, out string reasonCode); } public sealed class FileGroupActiveSelectionStore : IGroupActiveSelectionStore { private readonly struct Paths { internal string Primary { get; } internal string Temporary { get; } internal string Lock { get; } internal Paths(string primary, string temporary, string @lock) { Primary = primary; Temporary = temporary; Lock = @lock; } } private const uint Magic = 826361682u; private const ushort Schema = 1; private const int MaximumFileBytes = 2048; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); private readonly string _root; public FileGroupActiveSelectionStore(string root) { if (string.IsNullOrWhiteSpace(root)) { throw new ArgumentException("A storage root is required.", "root"); } _root = TrimTrailingSeparators(Path.GetFullPath(root)); if (string.Equals(_root, Path.GetPathRoot(_root), PathComparison())) { throw new ArgumentException("A filesystem root cannot be the active Group storage root.", "root"); } } public GroupActiveSelectionReadResult Read(string worldScope, StableIdentity identity) { if (identity == null) { return Unavailable("group-active-identity-missing"); } try { Paths paths = Resolve(worldScope, identity); if (!Directory.Exists(_root)) { return Missing(); } using (Acquire(paths.Lock)) { if (File.Exists(paths.Temporary)) { return Ambiguous("group-active-temporary-evidence"); } if (!File.Exists(paths.Primary)) { return Missing(); } Guid groupId; return TryDecode(ReadBounded(paths.Primary), worldScope, identity, out groupId) ? new GroupActiveSelectionReadResult(GroupActiveSelectionReadState.Ready, "group-active-ready", groupId) : Corrupt("group-active-corrupt"); } } catch (ArgumentException) { return Unavailable("group-active-request-invalid"); } catch (Exception exception) when (IsStorageFailure(exception)) { return Unavailable("group-active-read-unavailable"); } } public bool TrySet(string worldScope, StableIdentity identity, Guid groupId, out string reasonCode) { reasonCode = "group-active-write-unavailable"; if (identity == null) { reasonCode = "group-active-identity-missing"; return false; } try { Paths paths = Resolve(worldScope, identity); Directory.CreateDirectory(_root); using (Acquire(paths.Lock)) { byte[] array = Encode(worldScope, identity, groupId); using (FileStream fileStream = new FileStream(paths.Temporary, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)) { fileStream.Write(array, 0, array.Length); fileStream.Flush(flushToDisk: true); } byte[] array2 = ReadBounded(paths.Temporary); if (!ExactEquals(array, array2) || !TryDecode(array2, worldScope, identity, out var groupId2) || groupId2 != groupId) { reasonCode = "group-active-staged-readback-failed"; return false; } if (File.Exists(paths.Primary)) { File.Replace(paths.Temporary, paths.Primary, null, ignoreMetadataErrors: true); } else { File.Move(paths.Temporary, paths.Primary); } if (!TryDecode(ReadBounded(paths.Primary), worldScope, identity, out var groupId3) || groupId3 != groupId || File.Exists(paths.Temporary)) { reasonCode = "group-active-commit-readback-failed"; return false; } reasonCode = ((groupId == Guid.Empty) ? "group-active-cleared" : "group-active-selected"); return true; } } catch (ArgumentException) { reasonCode = "group-active-request-invalid"; return false; } catch (Exception exception) when (IsStorageFailure(exception)) { reasonCode = "group-active-write-unavailable"; return false; } } internal string GetPrimaryPath(string worldScope, StableIdentity identity) { return Resolve(worldScope, identity).Primary; } private Paths Resolve(string worldScope, StableIdentity identity) { worldScope = GroupIdentity.RequireWorldScope(worldScope); if (identity == null) { throw new ArgumentNullException("identity"); } byte[] bytes = StrictUtf8.GetBytes(worldScope + "\n" + identity.CanonicalKey); string text; using (SHA256 sHA = SHA256.Create()) { byte[] array = sHA.ComputeHash(bytes); StringBuilder stringBuilder = new StringBuilder(array.Length * 2); for (int i = 0; i < array.Length; i++) { stringBuilder.Append(array[i].ToString("x2")); } text = stringBuilder.ToString(); } string text2 = Path.Combine(_root, text + ".active"); if (!IsExactChild(text2, _root)) { throw new ArgumentException("The active Group path escaped its trusted root."); } return new Paths(text2, text2 + ".tmp", text2 + ".lock"); } private static byte[] Encode(string worldScope, StableIdentity identity, Guid groupId) { worldScope = GroupIdentity.RequireWorldScope(worldScope); if (identity == null) { throw new ArgumentNullException("identity"); } byte[] array; using (MemoryStream memoryStream = new MemoryStream()) { using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, StrictUtf8, leaveOpen: true); binaryWriter.Write(826361682u); binaryWriter.Write((ushort)1); WriteText(binaryWriter, worldScope, 128); WriteText(binaryWriter, identity.Authority, 64); WriteText(binaryWriter, identity.SubjectId, 1024); binaryWriter.Write(groupId.ToByteArray()); binaryWriter.Flush(); array = memoryStream.ToArray(); } byte[] array2; using (SHA256 sHA = SHA256.Create()) { array2 = sHA.ComputeHash(array); } byte[] array3 = new byte[array.Length + array2.Length]; Buffer.BlockCopy(array, 0, array3, 0, array.Length); Buffer.BlockCopy(array2, 0, array3, array.Length, array2.Length); if (array3.Length > 2048) { throw new InvalidDataException(); } return array3; } private static bool TryDecode(byte[] exact, string expectedWorld, StableIdentity expectedIdentity, out Guid groupId) { groupId = Guid.Empty; if (exact == null || exact.Length < 64 || exact.Length > 2048 || expectedIdentity == null) { return false; } int num = exact.Length - 32; byte[] array = new byte[num]; byte[] array2 = new byte[32]; Buffer.BlockCopy(exact, 0, array, 0, num); Buffer.BlockCopy(exact, num, array2, 0, array2.Length); byte[] right; using (SHA256 sHA = SHA256.Create()) { right = sHA.ComputeHash(array); } if (!ExactEquals(array2, right)) { return false; } try { using MemoryStream memoryStream = new MemoryStream(array, writable: false); using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8, leaveOpen: true); if (binaryReader.ReadUInt32() != 826361682 || binaryReader.ReadUInt16() != 1) { return false; } string a = ReadText(binaryReader, 128); string authority = ReadText(binaryReader, 64); string subjectId = ReadText(binaryReader, 1024); byte[] array3 = binaryReader.ReadBytes(16); if (array3.Length != 16 || memoryStream.Position != memoryStream.Length) { return false; } StableIdentity stableIdentity = new StableIdentity(authority, subjectId); if (!string.Equals(a, GroupIdentity.RequireWorldScope(expectedWorld), StringComparison.Ordinal) || !stableIdentity.Equals(expectedIdentity)) { return false; } groupId = new Guid(array3); return true; } catch (Exception ex) when (ex is ArgumentException || ex is IOException || ex is DecoderFallbackException) { return false; } } private static void WriteText(BinaryWriter writer, string value, int maximumBytes) { byte[] bytes = StrictUtf8.GetBytes(value ?? string.Empty); if (bytes.Length < 1 || bytes.Length > maximumBytes || bytes.Length > 65535) { throw new ArgumentOutOfRangeException("value"); } writer.Write((ushort)bytes.Length); writer.Write(bytes); } private static string ReadText(BinaryReader reader, int maximumBytes) { int num = reader.ReadUInt16(); if (num < 1 || num > maximumBytes || num > reader.BaseStream.Length - reader.BaseStream.Position) { throw new InvalidDataException(); } byte[] array = reader.ReadBytes(num); string text = StrictUtf8.GetString(array); if (!ExactEquals(array, StrictUtf8.GetBytes(text))) { throw new InvalidDataException(); } return text; } private static FileStream Acquire(string path) { return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, 1, FileOptions.WriteThrough); } private static byte[] ReadBounded(string path) { using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.SequentialScan); if (fileStream.Length < 1 || fileStream.Length > 2048) { throw new InvalidDataException(); } byte[] array = new byte[(int)fileStream.Length]; int num; for (int i = 0; i < array.Length; i += num) { num = fileStream.Read(array, i, array.Length - i); if (num <= 0) { throw new EndOfStreamException(); } } return array; } private static bool ExactEquals(byte[] left, byte[] right) { if (left == null || right ==