Skip to content

Commit b386e0a

Browse files
Copilotstephentoub
andcommitted
Fix base64 deserialization with JSON-escaped data in ImageContentBlock, AudioContentBlock, and BlobResourceContents
The ContentBlock.Converter and ResourceContents.Converter were using reader.ValueSpan.ToArray() to read base64-encoded data from JSON strings. This reads raw bytes without unescaping JSON escape sequences. When base64 data contains '/' characters that are JSON-escaped as '\/' (a valid JSON escape used by some encoders), the backslash corrupts the base64 data, causing "Invalid base64 data" FormatException on access to DecodedData. Fix: Check reader.ValueIsEscaped and fall back to GetString() + UTF8 encoding when escape sequences are present, preserving the fast path for the common unescaped case. Fixes #1340 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent 95acee6 commit b386e0a

4 files changed

Lines changed: 69 additions & 2 deletions

File tree

src/ModelContextProtocol.Core/Protocol/ContentBlock.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Diagnostics;
55
using System.Diagnostics.CodeAnalysis;
66
using System.Runtime.InteropServices;
7+
using System.Text;
78
using System.Text.Json;
89
using System.Text.Json.Nodes;
910
using System.Text.Json.Serialization;
@@ -137,7 +138,14 @@ public sealed class Converter : JsonConverter<ContentBlock>
137138
break;
138139

139140
case "data":
140-
data = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray();
141+
if (!reader.ValueIsEscaped)
142+
{
143+
data = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray();
144+
}
145+
else
146+
{
147+
data = Encoding.UTF8.GetBytes(reader.GetString()!);
148+
}
141149
break;
142150

143151
case "mimeType":

src/ModelContextProtocol.Core/Protocol/ResourceContents.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.ComponentModel;
33
using System.Diagnostics;
44
using System.Diagnostics.CodeAnalysis;
5+
using System.Text;
56
using System.Text.Json;
67
using System.Text.Json.Nodes;
78
using System.Text.Json.Serialization;
@@ -105,7 +106,14 @@ public sealed class Converter : JsonConverter<ResourceContents>
105106
break;
106107

107108
case "blob":
108-
blob = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray();
109+
if (!reader.ValueIsEscaped)
110+
{
111+
blob = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray();
112+
}
113+
else
114+
{
115+
blob = Encoding.UTF8.GetBytes(reader.GetString()!);
116+
}
109117
break;
110118

111119
case "text":

tests/ModelContextProtocol.Tests/Protocol/ContentBlockTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,4 +290,37 @@ public void AudioContentBlock_FromBytes_ThrowsForNullOrWhiteSpaceMimeType(string
290290
{
291291
Assert.ThrowsAny<ArgumentException>(() => AudioContentBlock.FromBytes((byte[])[1, 2, 3], mimeType!));
292292
}
293+
294+
[Fact]
295+
public void ImageContentBlock_Deserialization_HandlesEscapedForwardSlashInBase64()
296+
{
297+
// Base64 uses '/' which some JSON encoders escape as '\/' (valid JSON).
298+
// The converter must unescape before storing the base64 UTF-8 bytes.
299+
byte[] originalBytes = [0xFF, 0xD8, 0xFF, 0xE0]; // sample bytes that produce '/' in base64
300+
string base64 = Convert.ToBase64String(originalBytes); // "/9j/4A=="
301+
Assert.Contains("/", base64);
302+
303+
// Simulate a JSON encoder that escapes '/' as '\/'
304+
string json = $$"""{"type":"image","data":"{{base64.Replace("/", "\\/")}}","mimeType":"image/jpeg"}""";
305+
306+
var deserialized = JsonSerializer.Deserialize<ContentBlock>(json, McpJsonUtilities.DefaultOptions);
307+
var image = Assert.IsType<ImageContentBlock>(deserialized);
308+
Assert.Equal(base64, System.Text.Encoding.UTF8.GetString(image.Data.ToArray()));
309+
Assert.Equal(originalBytes, image.DecodedData.ToArray());
310+
}
311+
312+
[Fact]
313+
public void AudioContentBlock_Deserialization_HandlesEscapedForwardSlashInBase64()
314+
{
315+
byte[] originalBytes = [0xFF, 0xD8, 0xFF, 0xE0];
316+
string base64 = Convert.ToBase64String(originalBytes);
317+
Assert.Contains("/", base64);
318+
319+
string json = $$"""{"type":"audio","data":"{{base64.Replace("/", "\\/")}}","mimeType":"audio/wav"}""";
320+
321+
var deserialized = JsonSerializer.Deserialize<ContentBlock>(json, McpJsonUtilities.DefaultOptions);
322+
var audio = Assert.IsType<AudioContentBlock>(deserialized);
323+
Assert.Equal(base64, System.Text.Encoding.UTF8.GetString(audio.Data.ToArray()));
324+
Assert.Equal(originalBytes, audio.DecodedData.ToArray());
325+
}
293326
}

tests/ModelContextProtocol.Tests/Protocol/ResourceContentsTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,4 +489,22 @@ public static void BlobResourceContents_NullMimeType_OmittedFromJson()
489489

490490
Assert.DoesNotContain("mimeType", json);
491491
}
492+
493+
[Fact]
494+
public static void BlobResourceContents_Deserialization_HandlesEscapedForwardSlashInBase64()
495+
{
496+
// Base64 uses '/' which some JSON encoders escape as '\/' (valid JSON).
497+
// The converter must unescape before storing the base64 UTF-8 bytes.
498+
byte[] originalBytes = [0xFF, 0xD8, 0xFF, 0xE0]; // sample bytes that produce '/' in base64
499+
string base64 = Convert.ToBase64String(originalBytes); // "/9j/4A=="
500+
Assert.Contains("/", base64);
501+
502+
// Simulate a JSON encoder that escapes '/' as '\/'
503+
string json = $$"""{"uri":"file:///test.bin","blob":"{{base64.Replace("/", "\\/")}}","mimeType":"application/octet-stream"}""";
504+
505+
var deserialized = JsonSerializer.Deserialize<ResourceContents>(json, McpJsonUtilities.DefaultOptions);
506+
var blob = Assert.IsType<BlobResourceContents>(deserialized);
507+
Assert.Equal(base64, System.Text.Encoding.UTF8.GetString(blob.Blob.ToArray()));
508+
Assert.Equal(originalBytes, blob.DecodedData.ToArray());
509+
}
492510
}

0 commit comments

Comments
 (0)