Skip to content

Commit 35f6d1c

Browse files
committed
fix: Fix download flow, sparse conversion & parsing
Add tests for download error cases and sparse flashing; validate device-accepted download size and fail early on mismatches. Reuse buffers (ArrayPool/stacked transfer buffer) to reduce allocations and avoid extra copies during chunked downloads; improve stream read/write error handling and CRC parsing with clearer error responses. Implement RAW->sparse conversion for oversized images by writing a temp raw file (when needed), converting to sparse, and flashing via sparse protocol; ensure temp cleanup and stream repositioning. Make UdpTransport append packet bytes without extra ToArray allocations. Harden HandleResponse parsing: unify delimiter handling, use prefix helper checks, accumulate TEXT blocks in a StringBuilder, and return collected text with final responses.
1 parent 2dd6045 commit 35f6d1c

File tree

6 files changed

+254
-60
lines changed

6 files changed

+254
-60
lines changed

FirmwareKit.Comm.Fastboot.Tests/FastbootProtocolTests.cs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,16 +271,37 @@ public void HandleResponse_FragmentedDataPrefix_Succeeds()
271271
public void DownloadDataStream_PrematureEof_FailsEarly()
272272
{
273273
var transport = new MockTransport();
274-
transport.EnqueueResponse("DATA00000010");
274+
transport.EnqueueResponse("OKAYno");
275+
for (int i = 0; i < 4; i++)
276+
{
277+
transport.EnqueueResponse("DATA00000010");
278+
}
275279
var util = new FastbootDriver(transport);
276280

277281
using var shortStream = new MemoryStream(new byte[4]);
278282
var response = util.DownloadData(shortStream, 16);
279283

280284
Assert.Equal(FastbootState.Fail, response.Result);
285+
Assert.Contains("Max retries exceeded", response.Response);
281286
Assert.Contains("stream ended early", response.Response);
282287
}
283288

289+
[Fact]
290+
public void DownloadDataStream_DataSizeMismatch_Fails()
291+
{
292+
var transport = new MockTransport();
293+
transport.EnqueueResponse("OKAYno");
294+
transport.EnqueueResponse("DATA00000004");
295+
var util = new FastbootDriver(transport);
296+
297+
using var stream = new MemoryStream(new byte[8]);
298+
var response = util.DownloadData(stream, 8);
299+
300+
Assert.Equal(FastbootState.Fail, response.Result);
301+
Assert.Contains("download size mismatch", response.Response);
302+
Assert.DoesNotContain("Short write", response.Response);
303+
}
304+
284305
[Fact]
285306
public void DownloadDataBytes_ZeroLength_Fails()
286307
{
@@ -323,6 +344,35 @@ public void DownloadDataBytes_ShortWrite_Fails()
323344
Assert.Contains("Short write", response.Response);
324345
}
325346

347+
[Fact]
348+
public void DownloadDataBytes_DataSizeMismatch_Fails()
349+
{
350+
var transport = new MockTransport();
351+
transport.EnqueueResponse("DATA00000004");
352+
var util = new FastbootDriver(transport);
353+
354+
var response = util.DownloadData(new byte[8]);
355+
356+
Assert.Equal(FastbootState.Fail, response.Result);
357+
Assert.Contains("download size mismatch", response.Response);
358+
}
359+
360+
[Fact]
361+
public void DownloadDataStream_MalformedCrcResponse_Fails()
362+
{
363+
var transport = new MockTransport();
364+
transport.EnqueueResponse("OKAYyes");
365+
transport.EnqueueResponse("DATA00000004");
366+
transport.EnqueueResponse("OKAY0xGGGGGGGG");
367+
var util = new FastbootDriver(transport);
368+
369+
using var stream = new MemoryStream(new byte[] { 1, 2, 3, 4 });
370+
var response = util.DownloadData(stream, 4);
371+
372+
Assert.Equal(FastbootState.Fail, response.Result);
373+
Assert.Contains("invalid CRC response", response.Response);
374+
}
375+
326376
[Fact]
327377
public void UploadData_SegmentedRead_Succeeds()
328378
{
@@ -398,5 +448,27 @@ public void FlashImage_Stream_NonSparse_RewindsBeforeDownload()
398448
Assert.Contains("flash:boot", transport.Commands);
399449
Assert.Equal(imageBytes, transport.DownloadPayload.ToArray());
400450
}
451+
452+
[Fact]
453+
public void FlashUnsparseImage_OversizedRaw_IsConvertedToSparseAndFlashed()
454+
{
455+
byte[] imageBytes = new byte[128];
456+
var transport = new ProtocolDownloadCaptureTransport(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
457+
{
458+
["getvar:has-slot:boot"] = "OKAYno",
459+
["getvar:is-logical:boot"] = "OKAYno",
460+
["getvar:max-download-size"] = "OKAY0x40",
461+
["flash:boot"] = "OKAY"
462+
});
463+
464+
var util = new FastbootDriver(transport);
465+
466+
using var stream = new MemoryStream(imageBytes);
467+
var response = util.FlashUnsparseImage("boot", stream, stream.Length);
468+
469+
Assert.Equal(FastbootState.Success, response.Result);
470+
Assert.Contains(transport.Commands, c => c.StartsWith("download:", StringComparison.OrdinalIgnoreCase));
471+
Assert.Contains("flash:boot", transport.Commands);
472+
}
401473
}
402474
}

FirmwareKit.Comm.Fastboot/Backend/Network/UdpTransport.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,10 @@ private byte[] SendSinglePacket(PacketId id, ushort seq, PacketFlag flag, byte[]
142142

143143
if (rxPacket.Length > HeaderSize)
144144
{
145-
fullResponse.AddRange(rxPacket.AsSpan(HeaderSize).ToArray());
145+
for (int j = HeaderSize; j < rxPacket.Length; j++)
146+
{
147+
fullResponse.Add(rxPacket[j]);
148+
}
146149
}
147150

148151
gotValidResponse = true;

FirmwareKit.Comm.Fastboot/Command/InternalDownloadDataBytes.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,25 @@ public FastbootResponse DownloadData(byte[] data)
2121
FastbootResponse response = RawCommand("download:" + data.Length.ToString("x8"));
2222
if (response.Result != FastbootState.Data)
2323
return response;
24+
if (response.DataSize != data.Length)
25+
{
26+
return new FastbootResponse
27+
{
28+
Result = FastbootState.Fail,
29+
Response = $"download size mismatch: requested {data.Length}, device accepted {response.DataSize}"
30+
};
31+
}
2432

2533
long bytesWritten = 0;
2634
int length = data.Length;
35+
byte[] transferBuffer = new byte[Math.Min(OnceSendDataSize, length)];
2736

2837
while (bytesWritten < length)
2938
{
3039
int toWrite = (int)Math.Min(OnceSendDataSize, length - bytesWritten);
31-
byte[] chunk = new byte[toWrite];
32-
Array.Copy(data, bytesWritten, chunk, 0, toWrite);
40+
Buffer.BlockCopy(data, (int)bytesWritten, transferBuffer, 0, toWrite);
3341

34-
long written = Transport.Write(chunk, toWrite);
42+
long written = Transport.Write(transferBuffer, toWrite);
3543
if (written != toWrite)
3644
{
3745
return new FastbootResponse { Result = FastbootState.Fail, Response = $"Short write: {written}/{toWrite}" };

FirmwareKit.Comm.Fastboot/Command/InternalDownloadDataStream.cs

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
using Force.Crc32;
3+
using System.Buffers;
34

45
namespace FirmwareKit.Comm.Fastboot;
56

@@ -63,33 +64,47 @@ private FastbootResponse DownloadDataInternal(Stream stream, long length, bool o
6364
FastbootResponse response = RawCommand("download:" + length.ToString("x8"));
6465
if (response.Result != FastbootState.Data)
6566
return response;
67+
if (response.DataSize != length)
68+
{
69+
return new FastbootResponse
70+
{
71+
Result = FastbootState.Fail,
72+
Response = $"download size mismatch: requested {length}, device accepted {response.DataSize}"
73+
};
74+
}
6675

67-
byte[] buffer = new byte[OnceSendDataSize];
76+
byte[] buffer = ArrayPool<byte>.Shared.Rent(OnceSendDataSize);
6877
long bytesWritten = 0;
6978
uint crc = 0;
70-
71-
while (bytesWritten < length)
79+
try
7280
{
73-
int toRead = (int)Math.Min(OnceSendDataSize, length - bytesWritten);
74-
int readSize = stream.Read(buffer, 0, toRead);
75-
if (readSize <= 0)
81+
while (bytesWritten < length)
7682
{
77-
throw new Exception("stream ended early: " + bytesWritten + "/" + length);
78-
}
83+
int toRead = (int)Math.Min(OnceSendDataSize, length - bytesWritten);
84+
int readSize = stream.Read(buffer, 0, toRead);
85+
if (readSize <= 0)
86+
{
87+
throw new Exception("stream ended early: " + bytesWritten + "/" + length);
88+
}
7989

80-
if (useCrc)
81-
{
82-
crc = Crc32Algorithm.Append(crc, buffer, 0, readSize);
83-
}
90+
if (useCrc)
91+
{
92+
crc = Crc32Algorithm.Append(crc, buffer, 0, readSize);
93+
}
8494

85-
long written = Transport.Write(buffer, readSize);
86-
if (written != readSize)
87-
{
88-
throw new Exception("Short write: " + written + "/" + readSize);
95+
long written = Transport.Write(buffer, readSize);
96+
if (written != readSize)
97+
{
98+
throw new Exception("Short write: " + written + "/" + readSize);
99+
}
100+
bytesWritten += written;
101+
if (onEvent)
102+
NotifyProgress(bytesWritten, length);
89103
}
90-
bytesWritten += written;
91-
if (onEvent)
92-
NotifyProgress(bytesWritten, length);
104+
}
105+
finally
106+
{
107+
ArrayPool<byte>.Shared.Return(buffer);
93108
}
94109

95110
var finalRes = HandleResponse();
@@ -107,7 +122,14 @@ private FastbootResponse DownloadDataInternal(Stream stream, long length, bool o
107122
throw new Exception("CRC Mismatch: Device 0x" + deviceCrc.ToString("x8") + " != Host 0x" + crc.ToString("x8"));
108123
}
109124
}
110-
catch (FormatException) { }
125+
catch (FormatException)
126+
{
127+
return new FastbootResponse
128+
{
129+
Result = FastbootState.Fail,
130+
Response = "invalid CRC response from device: " + resp
131+
};
132+
}
111133
}
112134
}
113135

FirmwareKit.Comm.Fastboot/Command/InternalFlashUnsparseImage.cs

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11

22
using FirmwareKit.Sparse.Core;
3+
using FirmwareKit.Sparse.Utils;
4+
using System.Buffers;
35

46

57
namespace FirmwareKit.Comm.Fastboot;
@@ -49,29 +51,81 @@ public FastbootResponse FlashUnsparseImage(string partition, Stream stream, long
4951

5052
if (length > maxDownloadSize)
5153
{
52-
FastbootDebug.Log($"Large image detected, splitting RAW: {length} > {maxDownloadSize}");
53-
NotifyCurrentStep($"{partition} ({length} bytes) is larger than max-download-size ({maxDownloadSize}). Splitting RAW image...");
54-
// Manually split RAW stream into sparse-like chunks for download
55-
long bytesWritten = 0;
56-
int count = 1;
57-
long totalChunks = (length + maxDownloadSize - 1) / maxDownloadSize;
58-
59-
while (bytesWritten < length)
54+
NotifyCurrentStep($"{partition} ({length} bytes) exceeds max-download-size ({maxDownloadSize}). Converting RAW image to sparse...");
55+
string tempDir = Path.Combine(Path.GetTempPath(), "fastboot_sparse_" + Guid.NewGuid().ToString("N"));
56+
string sparsePath = Path.Combine(tempDir, "input.sparse.img");
57+
58+
try
6059
{
61-
long toWrite = Math.Min(maxDownloadSize, length - bytesWritten);
62-
NotifyCurrentStep($"Sending {partition} RAW chunk {count}/{totalChunks} ({toWrite} bytes)");
60+
Directory.CreateDirectory(tempDir);
6361

64-
// Create a sub-view of the RAW stream
65-
using var subStream = new SubStream(stream, bytesWritten, toWrite);
66-
DownloadData(subStream, toWrite).ThrowIfError();
62+
string? sourceRawPath = null;
63+
bool canUseFileDirectly = stream is FileStream fs &&
64+
fs.CanSeek &&
65+
originalPos == 0 &&
66+
length == fs.Length;
6767

68-
NotifyCurrentStep($"Flashing {partition} RAW chunk {count}/{totalChunks}");
69-
RawCommand("flash:" + partition).ThrowIfError();
68+
if (canUseFileDirectly)
69+
{
70+
sourceRawPath = ((FileStream)stream).Name;
71+
FastbootDebug.Log($"Converting RAW via direct source file: {sourceRawPath}");
72+
}
73+
else
74+
{
75+
string rawPath = Path.Combine(tempDir, "input.raw");
76+
sourceRawPath = rawPath;
77+
if (stream.CanSeek)
78+
{
79+
stream.Seek(originalPos, SeekOrigin.Begin);
80+
}
7081

71-
bytesWritten += toWrite;
72-
count++;
82+
using var ofs = File.Create(rawPath);
83+
byte[] copyBuffer = ArrayPool<byte>.Shared.Rent(1024 * 1024);
84+
try
85+
{
86+
long remaining = length;
87+
while (remaining > 0)
88+
{
89+
int toRead = (int)Math.Min(copyBuffer.Length, remaining);
90+
int read = stream.Read(copyBuffer, 0, toRead);
91+
if (read <= 0)
92+
{
93+
return new FastbootResponse
94+
{
95+
Result = FastbootState.Fail,
96+
Response = $"failed to read source RAW stream while converting to sparse: {length - remaining}/{length}"
97+
};
98+
}
99+
ofs.Write(copyBuffer, 0, read);
100+
remaining -= read;
101+
}
102+
}
103+
finally
104+
{
105+
ArrayPool<byte>.Shared.Return(copyBuffer);
106+
}
107+
}
108+
109+
SparseImageConverter.ConvertRawToSparse(sourceRawPath!, sparsePath, 4096);
110+
NotifyCurrentStep($"Converted RAW image to sparse. Flashing {partition} with sparse protocol...");
111+
return FlashSparseImage(partition, sparsePath);
112+
}
113+
catch (Exception ex)
114+
{
115+
return new FastbootResponse
116+
{
117+
Result = FastbootState.Fail,
118+
Response = "raw-to-sparse conversion failed: " + ex.Message
119+
};
120+
}
121+
finally
122+
{
123+
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
124+
if (stream.CanSeek)
125+
{
126+
try { stream.Seek(originalPos, SeekOrigin.Begin); } catch { }
127+
}
73128
}
74-
return new FastbootResponse { Result = FastbootState.Success };
75129
}
76130

77131
// AVB Footer logic

0 commit comments

Comments
 (0)