using Google.Protobuf.WellKnownTypes; using Grpc.Core; using ZB.MOM.WW.MxGateway.Contracts.Proto; namespace ZB.MOM.WW.MxGateway.Server.Sessions; /// /// Expands a client-supplied sparse array write () into a /// full, default-filled in place. /// /// /// MXAccess has no partial-array write primitive: a write replaces the whole array. /// Clients that only care about a few indices send an with /// the total length plus the indices they want to set; this expander materializes the /// full array so the worker can do an ordinary whole-array COM write. Indices the client /// did not mention are reset to the element type's default (they are NOT preserved from /// the live value); this is intentional, because the gateway cannot read-modify-write /// without racing the provider. /// /// For the MXAccess Integer element type the worker's COM-array converter (see /// VariantConverter.ConvertArray) chooses between a 32-bit and 64-bit sub-array /// based on the CLR element type. The sparse value carries no CLR array, so this expander /// mirrors that choice by inspecting the supplied element value kinds: if any element is an /// the whole array is emitted as /// ; otherwise it is emitted as /// (matching a default Integer array). /// internal static class SparseArrayExpander { /// /// Replaces 's with an /// equivalent full . If is not /// a sparse array this is a no-op, so callers may invoke it unconditionally. /// /// The value to expand in place. /// /// when the sparse payload is invalid: zero /// total length, an index at or beyond the total length, a duplicate index, an /// unsupported element type, or an element value whose kind does not match the declared /// element type. /// public static void Expand(MxValue value) { ArgumentNullException.ThrowIfNull(value); if (value.KindCase != MxValue.KindOneofCase.SparseArrayValue) { return; } MxSparseArray sparse = value.SparseArrayValue; MxDataType elementType = sparse.ElementDataType; uint totalLength = sparse.TotalLength; if (totalLength == 0) { throw Invalid("Sparse array total_length must be greater than zero."); } if (!IsSupportedElementType(elementType)) { throw Invalid($"Sparse array element_data_type '{elementType}' is not a supported scalar element type."); } if (totalLength > (uint)Array.MaxLength) { throw Invalid( $"Sparse array total_length {totalLength} exceeds the maximum supported array length {Array.MaxLength}."); } int length = (int)totalLength; HashSet seenIndices = new(); foreach (MxSparseElement element in sparse.Elements) { if (element.Index >= totalLength) { throw Invalid( $"Sparse array index {element.Index} is out of range for total_length {totalLength}."); } if (!seenIndices.Add(element.Index)) { throw Invalid($"Sparse array has a duplicate index {element.Index}."); } ValidateElementKind(elementType, element); } MxArray array = BuildArray(elementType, length, sparse.Elements); array.ElementDataType = elementType; array.Dimensions.Add(totalLength); // Assigning ArrayValue switches the oneof and clears SparseArrayValue. value.ArrayValue = array; } private static MxArray BuildArray( MxDataType elementType, int length, IReadOnlyList elements) { MxArray array = new(); switch (elementType) { case MxDataType.Boolean: { BoolArray values = new(); for (int i = 0; i < length; i++) { values.Values.Add(false); } foreach (MxSparseElement element in elements) { values.Values[(int)element.Index] = element.Value.BoolValue; } array.BoolValues = values; break; } case MxDataType.Integer when UsesInt64(elements): { Int64Array values = new(); for (int i = 0; i < length; i++) { values.Values.Add(0L); } foreach (MxSparseElement element in elements) { values.Values[(int)element.Index] = ReadInt64(element.Value); } array.Int64Values = values; break; } case MxDataType.Integer: { Int32Array values = new(); for (int i = 0; i < length; i++) { values.Values.Add(0); } foreach (MxSparseElement element in elements) { values.Values[(int)element.Index] = element.Value.Int32Value; } array.Int32Values = values; break; } case MxDataType.Float: { FloatArray values = new(); for (int i = 0; i < length; i++) { values.Values.Add(0f); } foreach (MxSparseElement element in elements) { values.Values[(int)element.Index] = element.Value.FloatValue; } array.FloatValues = values; break; } case MxDataType.Double: { DoubleArray values = new(); for (int i = 0; i < length; i++) { values.Values.Add(0d); } foreach (MxSparseElement element in elements) { values.Values[(int)element.Index] = element.Value.DoubleValue; } array.DoubleValues = values; break; } case MxDataType.String: { StringArray values = new(); for (int i = 0; i < length; i++) { values.Values.Add(string.Empty); } foreach (MxSparseElement element in elements) { values.Values[(int)element.Index] = element.Value.StringValue; } array.StringValues = values; break; } case MxDataType.Time: { TimestampArray values = new(); for (int i = 0; i < length; i++) { values.Values.Add(new Timestamp { Seconds = 0, Nanos = 0 }); } foreach (MxSparseElement element in elements) { values.Values[(int)element.Index] = element.Value.TimestampValue; } array.TimestampValues = values; break; } default: // Unreachable: IsSupportedElementType gates the element type before BuildArray. throw Invalid($"Sparse array element_data_type '{elementType}' is not supported."); } return array; } private static bool IsSupportedElementType(MxDataType elementType) => elementType switch { MxDataType.Boolean => true, MxDataType.Integer => true, MxDataType.Float => true, MxDataType.Double => true, MxDataType.String => true, MxDataType.Time => true, _ => false, }; private static void ValidateElementKind(MxDataType elementType, MxSparseElement element) { MxValue.KindOneofCase kind = element.Value?.KindCase ?? MxValue.KindOneofCase.None; bool matches = elementType switch { MxDataType.Boolean => kind == MxValue.KindOneofCase.BoolValue, MxDataType.Integer => kind is MxValue.KindOneofCase.Int32Value or MxValue.KindOneofCase.Int64Value, MxDataType.Float => kind == MxValue.KindOneofCase.FloatValue, MxDataType.Double => kind == MxValue.KindOneofCase.DoubleValue, MxDataType.String => kind == MxValue.KindOneofCase.StringValue, MxDataType.Time => kind == MxValue.KindOneofCase.TimestampValue, _ => false, }; if (!matches) { throw Invalid( $"Sparse array element at index {element.Index} has value kind '{kind}' which does not match element_data_type '{elementType}'."); } } private static bool UsesInt64(IReadOnlyList elements) { foreach (MxSparseElement element in elements) { if (element.Value.KindCase == MxValue.KindOneofCase.Int64Value) { return true; } } return false; } private static long ReadInt64(MxValue value) => value.KindCase == MxValue.KindOneofCase.Int64Value ? value.Int64Value : value.Int32Value; private static RpcException Invalid(string message) => new(new Status(StatusCode.InvalidArgument, message)); }