Skip to content

Commit fdca1a6

Browse files
committed
fix: Improve fastboot CLI and flash/fetch logic
Adds multiple CLI features and makes flash/fetch operations more robust. Key changes: - Extended FastbootCLI with new flags (-w, -S, --skip-reboot, --force, --fs-options, -a/--set-active), improved help text, improved output to stderr, and version bump. - Implemented chunked fetch handling in FastbootUtil.Fetch with automatic size detection and max-fetch-size support. - Enhanced FlashUnsparseImage to split large uploads into sparse chunks when exceeding max-download-size. - Introduced FastbootFlashAll to flash/update entire product directories or update zip files, handling dynamic/super partitions and image ordering. - Increased SparseMaxDownloadSize default to 1GB and added helper to set SparseMaxDownloadSize from CLI (-S). - Fixed WinUSB pipe policy to enable SHORT_PACKET_TERMINATE for Fastboot compliance and cleaned up using/imports. - Removed net9.0 target from project TFM list. These changes improve compatibility with AOSP fastboot behaviors, support large-image transfers, and provide convenience flags for common workflows.
1 parent c28ab8f commit fdca1a6

8 files changed

Lines changed: 392 additions & 76 deletions

File tree

FastbootCLI/Program.cs

Lines changed: 177 additions & 63 deletions
Large diffs are not rendered by default.

FirmwareKit.Comm.Fastboot/Command/Fetch.cs

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace FirmwareKit.Comm.Fastboot;
55
public partial class FastbootUtil
66
{
77
/// <summary>
8-
/// Fetches data from partition (fetch)
8+
/// Fetches data from partition (fetch), automatically handling large fetches in chunks
99
/// </summary>
1010
public FastbootResponse Fetch(string partition, string outputPath, long offset = 0, long size = -1)
1111
{
@@ -15,18 +15,54 @@ public FastbootResponse Fetch(string partition, string outputPath, long offset =
1515
targetPartition = partition + "_" + GetCurrentSlot();
1616
}
1717

18-
string cmd = "fetch:" + targetPartition;
18+
// If size is not specified, try to get it from variables
19+
if (size == -1)
20+
{
21+
string szVar = GetVar("partition-size:" + targetPartition);
22+
if (!string.IsNullOrEmpty(szVar) && szVar.StartsWith("0x"))
23+
size = Convert.ToInt64(szVar, 16);
24+
else if (!string.IsNullOrEmpty(szVar))
25+
size = long.Parse(szVar);
26+
}
27+
28+
long maxFetchSize = GetMaxFetchSize();
29+
if (size > 0 && maxFetchSize > 0 && size > maxFetchSize)
30+
{
31+
NotifyCurrentStep($"Partition {targetPartition} is larger than max-fetch-size, fetching in chunks...");
32+
using var fs = File.Create(outputPath);
33+
long fetched = 0;
34+
while (fetched < size)
35+
{
36+
long chunk = Math.Min(maxFetchSize, size - fetched);
37+
string cmd = $"fetch:{targetPartition}:0x{(offset + fetched):x8}:0x{chunk:x8}";
38+
var res = UploadData(cmd, fs);
39+
if (res.Result != FastbootState.Success) return res;
40+
fetched += chunk;
41+
NotifyProgress(fetched, size);
42+
}
43+
return new FastbootResponse { Result = FastbootState.Success };
44+
}
45+
46+
string finalCmd = "fetch:" + targetPartition;
1947
if (offset >= 0)
2048
{
21-
cmd += $":0x{offset:x8}";
49+
finalCmd += $":0x{offset:x8}";
2250
if (size >= 0)
2351
{
24-
cmd += $":0x{size:x8}";
52+
finalCmd += $":0x{size:x8}";
2553
}
2654
}
2755

28-
using var fs = File.Create(outputPath);
29-
return UploadData(cmd, fs);
56+
using var ofs = File.Create(outputPath);
57+
return UploadData(finalCmd, ofs);
58+
}
59+
60+
private long GetMaxFetchSize()
61+
{
62+
string val = GetVar("max-fetch-size");
63+
if (string.IsNullOrEmpty(val)) return -1;
64+
if (val.StartsWith("0x")) return Convert.ToInt64(val, 16);
65+
return long.Parse(val);
3066
}
3167

3268

FirmwareKit.Comm.Fastboot/Command/FlashUnsparseImage.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
using FirmwareKit.Comm.Fastboot.DataModel;
2+
using FirmwareKit.Sparse.Core;
23

34
namespace FirmwareKit.Comm.Fastboot;
45

56
public partial class FastbootUtil
67
{
78
/// <summary>
8-
/// Flashes non-sparse image (Already Error check)
9+
/// Flashes non-sparse image, automatically handling sparse split if larger than max-download-size
910
/// </summary>
1011
public FastbootResponse FlashUnsparseImage(string partition, Stream stream, long length)
1112
{
12-
NotifyCurrentStep($"Sending {partition}");
13+
long maxDownloadSize = GetMaxDownloadSize();
14+
if (length > maxDownloadSize)
15+
{
16+
NotifyCurrentStep($"{partition} is too large for a single download, splitting into sparse chunks...");
17+
// Use SparseFile to split raw image into manageable sparse chunks
18+
using var sfile = SparseFile.FromStream(stream);
19+
return FlashSparseFile(partition, sfile, maxDownloadSize);
20+
}
21+
22+
NotifyCurrentStep($"Sending {partition} ({length} bytes)");
1323
DownloadData(stream, length).ThrowIfError();
1424
NotifyCurrentStep($"Flashing {partition}");
1525
return RawCommand("flash:" + partition).ThrowIfError();
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
using System.IO.Compression;
2+
3+
namespace FirmwareKit.Comm.Fastboot;
4+
5+
public class FastbootFlashAll(FastbootUtil util)
6+
{
7+
private readonly FastbootUtil _util = util;
8+
private readonly ProductInfoParser _parser = new(util);
9+
10+
public void FlashUpdateZip(string zipPath, bool skipSecondary = false)
11+
{
12+
_util.NotifyCurrentStep("Extracting update zip...");
13+
string tempDir = Path.Combine(Path.GetTempPath(), "fastboot_update_" + Path.GetRandomFileName());
14+
Directory.CreateDirectory(tempDir);
15+
try
16+
{
17+
ZipFile.ExtractToDirectory(zipPath, tempDir);
18+
FlashFromDirectory(tempDir, skipSecondary);
19+
}
20+
finally
21+
{
22+
try { Directory.Delete(tempDir, true); } catch { }
23+
}
24+
}
25+
26+
public void FlashFromDirectory(string directory, bool skipSecondary = false)
27+
{
28+
string androidProductOut = directory;
29+
30+
// 1. Check android-info.txt
31+
string infoPath = Path.Combine(androidProductOut, "android-info.txt");
32+
if (File.Exists(infoPath))
33+
{
34+
_util.NotifyCurrentStep("Verifying device compatibility...");
35+
string content = File.ReadAllText(infoPath);
36+
if (!_parser.Validate(content, out string? error))
37+
{
38+
throw new Exception("Incompatible device: " + error);
39+
}
40+
}
41+
42+
// 2. Determine partitions to flash
43+
// Standard AOSP behavior: flash partitions in a specific order
44+
// and handle logical partitions via super_empty.img if present.
45+
46+
var partitions = GetPartitionList(androidProductOut);
47+
48+
// 3. Handle Super Partition Optimization (dynamic partitions)
49+
string superEmpty = Path.Combine(androidProductOut, "super_empty.img");
50+
if (File.Exists(superEmpty))
51+
{
52+
FlashDynamicPartitions(androidProductOut, superEmpty);
53+
}
54+
55+
// 4. Flash normal partitions
56+
foreach (var (part, file) in partitions)
57+
{
58+
if (IsDynamicPartition(part)) continue; // Already handled
59+
60+
_util.NotifyCurrentStep($"Flashing {part}...");
61+
FlashImage(part, file);
62+
}
63+
64+
_util.NotifyCurrentStep("Flash completed.");
65+
}
66+
67+
private void FlashDynamicPartitions(string directory, string superEmptyPath)
68+
{
69+
_util.NotifyCurrentStep("Flashing dynamic partitions...");
70+
var helper = new SuperFlashHelper(_util, "super", superEmptyPath);
71+
72+
// Find all images that should be in super
73+
// Usually these are system, vendor, product, system_ext, odm, etc.
74+
string[] dynamicPartitions = ["system", "vendor", "product", "system_ext", "odm", "vendor_dlkm", "odm_dlkm"];
75+
76+
bool addedAny = false;
77+
foreach (var p in dynamicPartitions)
78+
{
79+
string img = Path.Combine(directory, p + ".img");
80+
if (File.Exists(img))
81+
{
82+
helper.AddPartition(p, img);
83+
addedAny = true;
84+
}
85+
86+
// Handle A/B slots for dynamic partitions if needed
87+
string imgA = Path.Combine(directory, p + "_a.img");
88+
if (File.Exists(imgA))
89+
{
90+
helper.AddPartition(p + "_a", imgA);
91+
addedAny = true;
92+
}
93+
}
94+
95+
if (addedAny)
96+
{
97+
helper.Flash();
98+
}
99+
}
100+
101+
private void FlashImage(string partition, string filePath)
102+
{
103+
// Check if sparse
104+
if (IsSparseImage(filePath))
105+
{
106+
_util.FlashSparseImage(partition, filePath).ThrowIfError();
107+
}
108+
else
109+
{
110+
using var fs = File.OpenRead(filePath);
111+
_util.FlashUnsparseImage(partition, fs, fs.Length).ThrowIfError();
112+
}
113+
}
114+
115+
private bool IsSparseImage(string path)
116+
{
117+
using var fs = File.OpenRead(path);
118+
byte[] header = new byte[4];
119+
if (fs.Read(header, 0, 4) == 4)
120+
{
121+
return BitConverter.ToUInt32(header, 0) == 0xED26FF3A;
122+
}
123+
return false;
124+
}
125+
126+
private bool IsDynamicPartition(string name)
127+
{
128+
// Simple heuristic: if it's in the list of typical dynamic partitions
129+
string[] dynamic = ["system", "vendor", "product", "system_ext", "odm", "vendor_dlkm", "odm_dlkm"];
130+
return dynamic.Any(d => name == d || name.StartsWith(d + "_"));
131+
}
132+
133+
private List<(string partition, string file)> GetPartitionList(string directory)
134+
{
135+
var list = new List<(string, string)>();
136+
string[] priority = ["boot", "init_boot", "vendor_boot", "dtbo", "vbmeta", "vbmeta_system", "vbmeta_vendor", "recovery"];
137+
138+
foreach (var p in priority)
139+
{
140+
string path = Path.Combine(directory, p + ".img");
141+
if (File.Exists(path)) list.Add((p, path));
142+
}
143+
144+
// Add others found in directory
145+
foreach (var file in Directory.GetFiles(directory, "*.img"))
146+
{
147+
string name = Path.GetFileNameWithoutExtension(file);
148+
if (!priority.Contains(name) && name != "super_empty")
149+
{
150+
list.Add((name, file));
151+
}
152+
}
153+
154+
return list;
155+
}
156+
}

FirmwareKit.Comm.Fastboot/FastbootUtil.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public static LpMetadata ReadFromImageStream(Stream stream)
124124
public FastbootUtil(IFastbootTransport transport) => Transport = transport;
125125
public static int ReadTimeoutSeconds = 30;
126126
public static int OnceSendDataSize = 1024 * 1024;
127-
public static int SparseMaxDownloadSize = 256 * 1024 * 1024;
127+
public static int SparseMaxDownloadSize = 1024 * 1024 * 1024; // 1GB default limit to match AOSP RESPARSE_LIMIT
128128

129129
private static readonly string[] PartitionPriority = {
130130
"preloader", "bootloader", "radio", "dram", "md1img", "xbl", "abl", "keystore",

FirmwareKit.Comm.Fastboot/FirmwareKit.Comm.Fastboot.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0;net10.0</TargetFrameworks>
4+
<TargetFrameworks>netstandard2.0;netstandard2.1;net6.0;net8.0;net10.0</TargetFrameworks>
55
<LangVersion>latest</LangVersion>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>

FirmwareKit.Comm.Fastboot/Usb/Windows/LegacyUsbDevice.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
using System;
2-
using System.Runtime.InteropServices;
31
using System.ComponentModel;
2+
using System.Runtime.InteropServices;
43
using static FirmwareKit.Comm.Fastboot.Usb.Windows.Win32API;
54

65
namespace FirmwareKit.Comm.Fastboot.Usb.Windows

FirmwareKit.Comm.Fastboot/Usb/Windows/WinUSBDevice.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ public override int CreateHandle()
9393
WinUsb_SetPipePolicy(WinUSBHandle, WriteBulkID, AUTO_CLEAR_STALL, 1, ref bTrue);
9494
WinUsb_SetPipePolicy(WinUSBHandle, ReadBulkID, PIPE_TRANSFER_TIMEOUT, 4, ref timeout);
9595
WinUsb_SetPipePolicy(WinUSBHandle, WriteBulkID, PIPE_TRANSFER_TIMEOUT, 4, ref timeout);
96-
WinUsb_SetPipePolicy(WinUSBHandle, WriteBulkID, SHORT_PACKET_TERMINATE, 1, ref bFalse);
96+
// Enable SHORT_PACKET_TERMINATE (ZLP) for Fastboot compliance
97+
WinUsb_SetPipePolicy(WinUSBHandle, WriteBulkID, SHORT_PACKET_TERMINATE, 1, ref bTrue);
9798
return 0;
9899
}
99100

0 commit comments

Comments
 (0)