public void GetProductType(char t1, char t2, WmoBulletinProductTypes expectedTypes) { var actualType = WmoBulletinProductTypesHelper.GetProductTypes((byte)t1, (byte)t2); Assert.That(actualType, Is.EqualTo(expectedTypes)); }
private async IAsyncEnumerable <WmoBulletin> ReadPipeAsync(PipeReader reader, [EnumeratorCancellation] CancellationToken token) { var builders = new Dictionary <byte[], BulletinBuilder>(new ByteArrayEqualityComparer()); BulletinBuilder builder = null; var part = Part.None; byte marker = 0; int blockLength = 0; int reportLength = 0; int totalLength = 0; while (true) { var result = await reader.ReadAsync(token); if (result.IsCanceled) { break; } var data = result.Buffer; foreach (var bulletin in ProcessSequence()) { yield return(bulletin); } reader.AdvanceTo(data.Start, data.End); if (result.IsCompleted) { break; } IEnumerable <WmoBulletin> ProcessSequence() { bool canContinue = true; while (canContinue) { switch (part) { case Part.None: canContinue = ProcessPartNone(); break; case Part.FlagFieldSeparator when !data.IsEmpty: canContinue = ProcessFlagFieldSeparator(); break; case Part.BulletinHeading when !data.IsEmpty: canContinue = ProcessBulletinHeading(); break; case Part.Report: canContinue = ProcessReport(); break; case Part.End: if (!builder.IsMultipart || builder.Part == BulletinPart.Last) { yield return(builder.Build()); builder.Reset(); } part = Part.None; marker = 0; blockLength = 0; totalLength = 0; reportLength = 0; break; default: throw new EndOfStreamException("Unexpected end of stream at " + data.End.GetInteger()); } } bool ProcessPartNone() { if (data.IsEmpty) { return(false); } marker = data.First.Span[0]; if (marker != StarMarker && marker != HashMarker) { throw GetInvalidFormatException(data.Start); } part = Part.FlagFieldSeparator; return(true); } bool ProcessFlagFieldSeparator() { int length = marker == StarMarker ? 19 : Math.Max(18, blockLength); if (data.Length < length) { return(false); } Span <byte> span = stackalloc byte[length]; data.Slice(0, length).CopyTo(span); if (marker == StarMarker) { ValidateMarkers(span, 0, StarMarkers); totalLength = GetNumber(span.Slice(4, 10), data.GetPosition(4)); ValidateMarkers(span, 14, StarMarkers); if (span[18] != (byte)'\n') { throw GetInvalidFormatException(data.GetPosition(18)); } } else { ValidateMarkers(span, 0, HashMarkers); blockLength = GetNumber(span.Slice(4, 3), data.Start); if (blockLength > length) { return(true); // We will allocate enough buffer on next loop. } bool isVariable = blockLength > 18; totalLength = GetNumber(span.Slice(7, isVariable ? 11 : 6), data.GetPosition(7)); ValidateMarkers(span, blockLength - 5, HashMarkers); if (span[blockLength - 1] != (byte)'\n') { throw GetInvalidFormatException(data.GetPosition(18)); } } data = data.Slice(length); part = Part.BulletinHeading; return(true); void ValidateMarkers(in ReadOnlySpan <byte> span, int offset, in ReadOnlyMemory <byte> pattern) { if (!span.Slice(offset, pattern.Length).SequenceEqual(pattern.Span)) { throw GetInvalidFormatException(data.GetPosition(offset)); } } } bool ProcessBulletinHeading() { var lineEnd = data.PositionOf((byte)'\n'); if (lineEnd == null) { return(false); } var end = data.PositionOf((byte)'\r'); if (end == null || !data.GetPosition(2, end.Value).Equals(lineEnd.Value)) { throw GetInvalidFormatException(lineEnd.Value); } var slice = data.Slice(data.Start, end.Value); if (slice.IsSingleSegment) { BuildHeading(slice.First.Span); } else { Span <byte> span = stackalloc byte[(int)slice.Length]; slice.CopyTo(span); BuildHeading(span); } reportLength = totalLength - (int)slice.Length - 3; data = data.Slice(lineEnd.Value).Slice(1); part = Part.Report; return(true); void BuildHeading(in ReadOnlySpan <byte> span) { if (span.Length < 18) { throw GetInvalidFormatException(end.Value); } var type = WmoBulletinType.Normal; bool multipart = false; ushort index = 0; if (span.Length >= 20) { if (span.Length < 22) { throw GetInvalidFormatException(end.Value); } switch (span[19]) { case (byte)'C': type = WmoBulletinType.Correction; index = span[21]; break; case (byte)'R': type = WmoBulletinType.Delayed; index = span[21]; break; case (byte)'A': type = WmoBulletinType.Amendment; index = span[21]; break; case (byte)'P': multipart = true; var key = span.Slice(0, 18).ToArray(); if (builders.TryGetValue(key, out builder)) { builder.Part = span[20] == (byte)'Z' ? BulletinPart.Last : BulletinPart.Middle; if (builder.Part == BulletinPart.Last) { builders.Remove(key); } } else { builder = new BulletinBuilder(this) { IsMultipart = true, Part = BulletinPart.First }; builders.Add(key, builder); } index = (ushort)((ushort)(span[20] << 8) | (ushort)span[21]); if (index < builder.Index) { throw GetInvalidFormatException(data.GetPosition(20), "Invalid message part order."); } break; } } if (builder == null || (builder.IsMultipart && !multipart)) { builder = new BulletinBuilder(this); } builder.Type = type; builder.Index = index; builder.T1 = span[0]; builder.T2 = span[1]; builder.A1 = span[2]; builder.A2 = span[3]; builder.ii = checked ((byte)((span[4] - '0') * 10 + (span[5] - '0'))); builder.Location = BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(7, 4)); int day = GetNumber(span.Slice(12, 2), data.GetPosition(12)); int hour = GetNumber(span.Slice(14, 2), data.GetPosition(14)); int minute = GetNumber(span.Slice(16, 2), data.GetPosition(16)); builder.Time = new DayHourMinute(day, hour, minute); var productTypes = WmoBulletinProductTypesHelper.GetProductTypes(builder.T1, builder.T2); byte productTypesBits = (byte)productTypes; if (productTypesBits == 0) { builder.ProductType = _expectedProductType ?? throw new InvalidOperationException(); } else if (IsPowerOf2(productTypesBits)) { var productType = FromBit(productTypes); builder.ProductType = (_expectedProductType == null || _expectedProductType == productType) ? productType : throw new InvalidOperationException(); } else if (_expectedProductType != null && (productTypesBits & (1 << (byte)_expectedProductType.Value)) != 0) { builder.ProductType = _expectedProductType.Value; } else { throw new InvalidOperationException(); } } } bool ProcessReport() { if (data.Length < reportLength) { if (result.IsCompleted) { part = Part.End; return(true); } return(false); } var reportSlice = data.Slice(data.Start, reportLength); if (_supplementaryIdentificationLineResolver != null) { var supplementaryIdentificationLine = _supplementaryIdentificationLineResolver.Invoke(ref reportSlice, builder.T1, builder.T2); builder.SupplementaryIdentificationLine ??= supplementaryIdentificationLine; } switch (builder.ProductType) { case WmoBulletinProductType.DecodableText: SetDecodableTextReport(); break; case WmoBulletinProductType.PlainText: case WmoBulletinProductType.Xml: SetPlainTextReport(); break; case WmoBulletinProductType.Binary: SetBinaryReport(); break; default: throw new ArgumentOutOfRangeException(); } data = data.Slice(reportLength); part = Part.End; return(true); void SetDecodableTextReport() { do { var(end, isBulletinEnd) = GetEndPosition(); var slice = reportSlice.Slice(reportSlice.Start, end); string report; if (slice.IsSingleSegment) { report = GetAsciiString(slice.First.Span); } else { Span <byte> span = stackalloc byte[(int)slice.Length]; slice.CopyTo(span); report = GetAsciiString(span); } builder.TextReports.Add(report); if (isBulletinEnd) { break; } reportSlice = reportSlice.Slice(end).Slice(1); } while (!reportSlice.IsEmpty); } void SetPlainTextReport() { string report; if (reportSlice.IsSingleSegment) { report = GetAsciiString(reportSlice.First.Span); } else { Span <byte> span = stackalloc byte[(int)reportSlice.Length]; reportSlice.CopyTo(span); report = GetAsciiString(span); } builder.TextReports.Add(report); } void SetBinaryReport() { Span <byte> span; if (builder.BinaryReport == null) { builder.BinaryReport = new byte[reportSlice.Length]; span = builder.BinaryReport; } else { var binaryReport = builder.BinaryReport; var initialLength = binaryReport.Length; Array.Resize(ref binaryReport, initialLength + (int)reportSlice.Length); builder.BinaryReport = binaryReport; span = builder.BinaryReport.AsSpan(initialLength); } reportSlice.CopyTo(span); } (SequencePosition end, bool isBulletinEnd) GetEndPosition() { var reportEnd = reportSlice.PositionOf((byte)'='); return(reportEnd != null ? (reportEnd.Value, false) : (reportSlice.End, true)); } } Exception GetInvalidFormatException(in SequencePosition pos, string msg = null) { return(new InvalidOperationException("Invalid file format")); // TODO: Use custom exception }