Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions src/EncDotNet.Iso8211/Iso8211DataDescriptiveRecordReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -697,8 +697,15 @@ private static Iso8211SubfieldFormat ParseBitStringFormat(string format, int pos
}

/// <summary>
/// Parses a binary format like <c>b11</c>, <c>b12</c>, <c>b14</c>, <c>b21</c>, <c>b22</c>, <c>b24</c>.
/// Parses a binary format like <c>b11</c>, <c>b12</c>, <c>b14</c>, <c>b21</c>, <c>b22</c>, <c>b24</c>,
/// or <c>b48</c>.
/// </summary>
/// <remarks>
/// The binary format is <c>b</c> followed by two digits: the first digit is the data type
/// (<c>1</c> unsigned integer, <c>2</c> signed integer, <c>4</c>/<c>5</c> floating-point real) and
/// the second digit is the width in bytes. The width is honored as written so that, for example,
/// the 8-byte <c>b48</c> control is read as 8 bytes rather than being truncated or dropped.
/// </remarks>
private static Iso8211SubfieldFormat? ParseBinaryFormat(string format, int pos)
{
pos++; // Skip 'b'
Expand All @@ -708,16 +715,16 @@ private static Iso8211SubfieldFormat ParseBitStringFormat(string format, int pos
return null;
}

// Parse sign indicator: 1 = unsigned, 2 = signed
var signIndicator = format[pos] - '0';
// Parse data type indicator: 1 = unsigned, 2 = signed, 4/5 = floating-point.
var typeIndicator = format[pos] - '0';
pos++;

if (pos >= format.Length)
{
return null;
}

// Parse byte width
// Parse byte width (the remaining digit(s), e.g. the '8' in 'b48').
int width = 0;
while (pos < format.Length && char.IsDigit(format[pos]))
{
Expand All @@ -726,14 +733,18 @@ private static Iso8211SubfieldFormat ParseBitStringFormat(string format, int pos
}

Iso8211SubfieldFormatType formatType;
switch (signIndicator)
switch (typeIndicator)
{
case 1:
formatType = Iso8211SubfieldFormatType.UnsignedInteger;
break;
case 2:
formatType = Iso8211SubfieldFormatType.SignedInteger;
break;
case 4:
case 5:
formatType = Iso8211SubfieldFormatType.FloatingPoint;
break;
default:
return null;
}
Expand Down
44 changes: 44 additions & 0 deletions src/EncDotNet.Iso8211/Iso8211FieldReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ private T ConvertValue<T>(ParsedSubfield parsed, Iso8211SubfieldDefinition subfi
Iso8211SubfieldFormatType.Real => ConvertAsciiReal<T>(span),
Iso8211SubfieldFormatType.UnsignedInteger => ConvertUnsignedBinary<T>(span, format.Width),
Iso8211SubfieldFormatType.SignedInteger => ConvertSignedBinary<T>(span, format.Width),
Iso8211SubfieldFormatType.FloatingPoint => ConvertFloatingBinary<T>(span, format.Width),
Iso8211SubfieldFormatType.BitString => ConvertBitString<T>(span),
_ => throw new InvalidOperationException($"Unknown format type: {format.FormatType}")
};
Expand Down Expand Up @@ -810,6 +811,49 @@ private static object ConvertSignedBinary<T>(ReadOnlySpan<byte> data, int width)
throw new InvalidOperationException($"Cannot convert signed binary to type {typeof(T).Name}.");
}

/// <summary>
/// Converts floating-point binary data (ISO 8211 <c>b4x</c>/<c>b5x</c>, IEEE 754) to the requested type.
/// </summary>
private static object ConvertFloatingBinary<T>(ReadOnlySpan<byte> data, int width)
{
int actualWidth = Math.Min(width, data.Length);

double value = actualWidth switch
{
4 => BinaryPrimitives.ReadSingleLittleEndian(data),
8 => BinaryPrimitives.ReadDoubleLittleEndian(data),
_ => throw new InvalidOperationException(
$"Unsupported floating-point binary width: {actualWidth} byte(s).")
};

if (typeof(T) == typeof(double))
{
return value;
}
if (typeof(T) == typeof(float))
{
return (float)value;
}
if (typeof(T) == typeof(decimal))
{
return (decimal)value;
}
if (typeof(T) == typeof(int))
{
return (int)value;
}
if (typeof(T) == typeof(long))
{
return (long)value;
}
if (typeof(T) == typeof(string))
{
return value.ToString(CultureInfo.InvariantCulture);
}

throw new InvalidOperationException($"Cannot convert floating-point binary to type {typeof(T).Name}.");
}

/// <summary>
/// Converts bit string data (ISO 8211 <c>B(n)</c> format) to the requested type.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/EncDotNet.Iso8211/Iso8211SubfieldFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public readonly record struct Iso8211SubfieldFormat
{
Iso8211SubfieldFormatType.UnsignedInteger => Width,
Iso8211SubfieldFormatType.SignedInteger => Width,
Iso8211SubfieldFormatType.FloatingPoint => Width,
Iso8211SubfieldFormatType.BitString => Width,
_ => Width
};
Expand All @@ -78,6 +79,7 @@ public readonly record struct Iso8211SubfieldFormat
Iso8211SubfieldFormatType.Real => Width > 0 ? $"R({Width})" : "R",
Iso8211SubfieldFormatType.UnsignedInteger => $"b1{Width}",
Iso8211SubfieldFormatType.SignedInteger => $"b2{Width}",
Iso8211SubfieldFormatType.FloatingPoint => $"b4{Width}",
Iso8211SubfieldFormatType.BitString => $"B({Width * 8})",
_ => $"?({Width})"
};
Expand Down
9 changes: 8 additions & 1 deletion src/EncDotNet.Iso8211/Iso8211SubfieldFormatType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,12 @@ public enum Iso8211SubfieldFormatType : byte
/// Bit string data. Corresponds to format code <c>B(n)</c> where n is the width in bits.
/// The <see cref="Iso8211SubfieldFormat.Width"/> is stored in bytes (n / 8).
/// </summary>
BitString = 5
BitString = 5,

/// <summary>
/// Floating-point binary (IEEE 754) data. Corresponds to format codes <c>b4x</c> and
/// <c>b5x</c> where x is the byte width (4 for single precision, 8 for double precision).
/// Used, for example, by the S-100 DSSI origin-shift subfields (<c>b48</c>).
/// </summary>
FloatingPoint = 6
}
80 changes: 80 additions & 0 deletions tests/EndDotNet.UnitTests/Iso8211FieldReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1320,4 +1320,84 @@ public void ReadConcatenatedArrayWithLeaderAndRepeatingGroups()
}

#endregion

#region Regression Tests - 8-byte Binary Format Controls (b48)

/// <summary>
/// Regression test for issue #9: <see cref="Iso8211FieldReader"/> mis-parsed binary
/// format controls of the form <c>bXY</c> where the width digit is 8 (e.g. <c>b48</c>).
/// </summary>
/// <remarks>
/// The S-100 / S-101 DSSI field declares <c>(3b48,10b14)</c>: three 8-byte floating-point
/// origin shifts (<c>DCOX/DCOY/DCOZ</c>) followed by ten 4-byte unsigned integers. Previously
/// the leading <c>b48</c> subfields were dropped (or read as 4 bytes), shifting every later
/// subfield by 12 bytes and decoding <c>CMFX/CMFY/CMFZ</c> as 0. This test parses the raw
/// 64-byte DSSI blob from the issue and asserts the correctly aligned values.
/// </remarks>
[Fact]
public void GetSubfield_DssiFieldWith8ByteBinaryControls_DecodesCorrectly()
{
// Arrange — the real DSSI format control read back from a cell's DDR.
var formats = Iso8211DataDescriptiveRecordReader.ParseFormatControls("(3b48,10b14)");

var names = new[]
{
"DCOX", "DCOY", "DCOZ",
"CMFX", "CMFY", "CMFZ",
"NOIR", "NOPN", "NOMN", "NOCN", "NOXN", "NOSN", "NOFR"
};

// The three leading subfields must be 8-byte floating-point; the rest 4-byte unsigned.
Assert.Equal(13, formats.Length);
for (int i = 0; i < 3; i++)
{
Assert.Equal(Iso8211SubfieldFormatType.FloatingPoint, formats[i].FormatType);
Assert.Equal(8, formats[i].Width);
}
for (int i = 3; i < 13; i++)
{
Assert.Equal(Iso8211SubfieldFormatType.UnsignedInteger, formats[i].FormatType);
Assert.Equal(4, formats[i].Width);
}

var definitions = ImmutableArray.CreateBuilder<Iso8211SubfieldDefinition>();
for (int i = 0; i < names.Length; i++)
{
definitions.Add(new Iso8211SubfieldDefinition
{
Name = names[i],
Format = formats[i],
Index = i,
IsRepeating = false
});
}

var fieldDef = new Iso8211FieldDefinition
{
Tag = "DSSI",
DataStructureCode = Iso8211DataStructureCode.Vector,
DataTypeCode = Iso8211DataTypeCode.MixedDataTypes,
FieldName = "DSSI",
FormatControls = "(3b48,10b14)",
SubfieldDefinitions = definitions.ToImmutable(),
RepeatingSubfieldStartIndex = -1
};

var data = Convert.FromHexString(
"00000000000000000000000000000000000000000000000080969800809698006400000055000000C012000005000000451500002B0500007A0400000E0C0000");

Assert.Equal(64, data.Length);

var reader = new Iso8211FieldReader(fieldDef, data);

// Act & Assert — the multiplication factors must align past the three 8-byte shifts.
Assert.Equal(10000000u, reader.GetSubfield<uint>("CMFX"));
Assert.Equal(10000000u, reader.GetSubfield<uint>("CMFY"));
Assert.Equal(100u, reader.GetSubfield<uint>("CMFZ"));
Assert.Equal(85u, reader.GetSubfield<uint>("NOIR"));
Assert.Equal(4800u, reader.GetSubfield<uint>("NOPN"));
Assert.Equal(5u, reader.GetSubfield<uint>("NOMN"));
}

#endregion
}
Loading