diff --git a/src/wpiutil/Serialization/Struct/DynamicStruct.cs b/src/wpiutil/Serialization/Struct/DynamicStruct.cs index de16b51b..01bf0eec 100644 --- a/src/wpiutil/Serialization/Struct/DynamicStruct.cs +++ b/src/wpiutil/Serialization/Struct/DynamicStruct.cs @@ -128,10 +128,65 @@ public string GetStringField(StructFieldDescriptor field) } ReadOnlySpan bytes = Buffer.Span[field.Offset..field.ArraySize]; - return Encoding.UTF8.GetString(bytes); + + // Find last non zero character + int stringLength = bytes.Length; + for (; stringLength > 0; stringLength--) + { + if (bytes[stringLength - 1] != 0) + { + break; + } + } + // If string is all zeroes, its empty and return an empty string. + if (stringLength == 0) + { + return ""; + } + // Check if the end of the string is in the middle of a continuation byte or not. + if ((bytes[stringLength - 1] & 0x80) != 0) + { + // This is a UTF8 continuation byte. Make sure its valid. + // Walk back until initial byte is found + int utf8StartByte = stringLength; + for (; utf8StartByte > 0; utf8StartByte--) + { + if ((bytes[utf8StartByte - 1] & 0x40) != 0) + { + // Having 2nd bit set means start byte + break; + } + } + if (utf8StartByte == 0) + { + // This case means string only contains continuation bytes + return ""; + } + utf8StartByte--; + // Check if its a 2, 3, or 4 byte + byte checkByte = bytes[utf8StartByte]; + if ((checkByte & 0xE0) == 0xC0 && utf8StartByte != stringLength - 2) + { + // 2 byte, need 1 more byte + stringLength = utf8StartByte; + } + else if ((checkByte & 0xF0) == 0xE0 && utf8StartByte != stringLength - 3) + { + // 3 byte, need 2 more bytes + stringLength = utf8StartByte; + } + else if ((checkByte & 0xF8) == 0xF0 && utf8StartByte != stringLength - 4) + { + // 4 byte, need 3 more bytes + stringLength = utf8StartByte; + } + // If we get here, the string is either completely garbage or fine. + } + + return Encoding.UTF8.GetString(bytes[..stringLength]); } - public void SetStringField(StructFieldDescriptor field, string value) + public bool SetStringField(StructFieldDescriptor field, string value) { if (field.Type.Type != StructFieldType.Char) { @@ -147,9 +202,9 @@ public void SetStringField(StructFieldDescriptor field, string value) } Span bytes = Buffer.Span[field.Offset..field.ArraySize]; - Encoding.UTF8.GetEncoder().Convert(value, bytes, false, out int _, out int bytesUsed, out bool _); + Encoding.UTF8.GetEncoder().Convert(value, bytes, false, out int _, out int bytesUsed, out bool complete); bytes[bytesUsed..].Clear(); - + return complete; } public DynamicStruct GetStructField(StructFieldDescriptor field, int arrIndex = 0) diff --git a/test/wpiutil.test/DynamicStructTest.cs b/test/wpiutil.test/DynamicStructTest.cs index ebe92442..1b243099 100644 --- a/test/wpiutil.test/DynamicStructTest.cs +++ b/test/wpiutil.test/DynamicStructTest.cs @@ -27,4 +27,159 @@ public void TestNestedStruct() Assert.True(desc2.IsValid); Assert.Equal(4, desc2.Size); } + + [Fact] + public void TestStringAllZeros() + { + var db = new StructDescriptorDatabase(); + var desc = db.Add("test", "char a[32]"); + var dynamic = DynamicStruct.Allocate(desc); + var field = desc.FindFieldByName("a"); + Assert.NotNull(field); + Assert.Equal("", dynamic.GetStringField(field)); + } + + [Fact] + public void TestStringRoundTrip() + { + var db = new StructDescriptorDatabase(); + var desc = db.Add("test", "char a[32]"); + var dynamic = DynamicStruct.Allocate(desc); + var field = desc.FindFieldByName("a"); + Assert.NotNull(field); + Assert.True(dynamic.SetStringField(field, "abc")); + Assert.Equal("abc", dynamic.GetStringField(field)); + } + + [Fact] + public void TestStringRoundTripEmbeddedNull() + { + var db = new StructDescriptorDatabase(); + var desc = db.Add("test", "char a[32]"); + var dynamic = DynamicStruct.Allocate(desc); + var field = desc.FindFieldByName("a"); + Assert.NotNull(field); + Assert.True(dynamic.SetStringField(field, "ab\0c")); + Assert.Equal("ab\0c", dynamic.GetStringField(field)); + } + + [Fact] + public void TestStringRoundTripStringTooLong() + { + var db = new StructDescriptorDatabase(); + var desc = db.Add("test", "char a[2]"); + var dynamic = DynamicStruct.Allocate(desc); + var field = desc.FindFieldByName("a"); + Assert.NotNull(field); + Assert.False(dynamic.SetStringField(field, "abc")); + Assert.Equal("ab", dynamic.GetStringField(field)); + } + + [Fact] + public void TestStringRoundTripPartial2ByteUtf8() + { + var db = new StructDescriptorDatabase(); + var desc = db.Add("test", "char a[2]"); + var dynamic = DynamicStruct.Allocate(desc); + var field = desc.FindFieldByName("a"); + Assert.NotNull(field); + Assert.False(dynamic.SetStringField(field, "a\u0234")); + Assert.Equal("a", dynamic.GetStringField(field)); + } + + [Fact] + public void TestStringRoundTrip2ByteUtf8() + { + var db = new StructDescriptorDatabase(); + var desc = db.Add("test", "char a[3]"); + var dynamic = DynamicStruct.Allocate(desc); + var field = desc.FindFieldByName("a"); + Assert.NotNull(field); + Assert.True(dynamic.SetStringField(field, "a\u0234")); + Assert.Equal("a\u0234", dynamic.GetStringField(field)); + } + + [Fact] + public void TestStringRoundTripPartial3ByteUtf8FirstByte() + { + var db = new StructDescriptorDatabase(); + var desc = db.Add("test", "char a[2]"); + var dynamic = DynamicStruct.Allocate(desc); + var field = desc.FindFieldByName("a"); + Assert.NotNull(field); + Assert.False(dynamic.SetStringField(field, "a\u1234")); + Assert.Equal("a", dynamic.GetStringField(field)); + } + + [Fact] + public void TestStringRoundTripPartial3ByteUtf8SecondByte() + { + var db = new StructDescriptorDatabase(); + var desc = db.Add("test", "char a[3]"); + var dynamic = DynamicStruct.Allocate(desc); + var field = desc.FindFieldByName("a"); + Assert.NotNull(field); + Assert.False(dynamic.SetStringField(field, "a\u1234")); + Assert.Equal("a", dynamic.GetStringField(field)); + } + + [Fact] + public void TestStringRoundTrip3ByteUtf8() + { + var db = new StructDescriptorDatabase(); + var desc = db.Add("test", "char a[4]"); + var dynamic = DynamicStruct.Allocate(desc); + var field = desc.FindFieldByName("a"); + Assert.NotNull(field); + Assert.True(dynamic.SetStringField(field, "a\u1234")); + Assert.Equal("a\u1234", dynamic.GetStringField(field)); + } + + [Fact] + public void TestStringRoundTripPartial4ByteUtf8FirstByte() + { + var db = new StructDescriptorDatabase(); + var desc = db.Add("test", "char a[2]"); + var dynamic = DynamicStruct.Allocate(desc); + var field = desc.FindFieldByName("a"); + Assert.NotNull(field); + Assert.False(dynamic.SetStringField(field, "a\uD83D\uDC00")); + Assert.Equal("a", dynamic.GetStringField(field)); + } + + [Fact] + public void TestStringRoundTripPartial4ByteUtf8SecondByte() + { + var db = new StructDescriptorDatabase(); + var desc = db.Add("test", "char a[3]"); + var dynamic = DynamicStruct.Allocate(desc); + var field = desc.FindFieldByName("a"); + Assert.NotNull(field); + Assert.False(dynamic.SetStringField(field, "a\uD83D\uDC00")); + Assert.Equal("a", dynamic.GetStringField(field)); + } + + [Fact] + public void TestStringRoundTripPartial4ByteUtf8ThirdByte() + { + var db = new StructDescriptorDatabase(); + var desc = db.Add("test", "char a[4]"); + var dynamic = DynamicStruct.Allocate(desc); + var field = desc.FindFieldByName("a"); + Assert.NotNull(field); + Assert.False(dynamic.SetStringField(field, "a\uD83D\uDC00")); + Assert.Equal("a", dynamic.GetStringField(field)); + } + + [Fact] + public void TestStringRoundTrip4ByteUtf8() + { + var db = new StructDescriptorDatabase(); + var desc = db.Add("test", "char a[5]"); + var dynamic = DynamicStruct.Allocate(desc); + var field = desc.FindFieldByName("a"); + Assert.NotNull(field); + Assert.True(dynamic.SetStringField(field, "a\uD83D\uDC00")); + Assert.Equal("a\uD83D\uDC00", dynamic.GetStringField(field)); + } }