From dbdcaa4c74e934fa8c17993cc743c57d62ca21a3 Mon Sep 17 00:00:00 2001 From: Umer Uddin Date: Sat, 13 Dec 2025 14:26:33 +0000 Subject: [PATCH 1/2] Add minimal sparse handler Needed for reading and splitting sparse images to be properly flashed. Signed-off-by: Umer Uddin --- Sparse.cs | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 Sparse.cs diff --git a/Sparse.cs b/Sparse.cs new file mode 100644 index 0000000..3458663 --- /dev/null +++ b/Sparse.cs @@ -0,0 +1,121 @@ +using System; +using System.IO; + +namespace FastBoot +{ + internal class Sparse + { + public static readonly uint sparse_magic = 0xED26FF3A; + + public enum chunkTypes + { + RAW = 0xCAC1, + FILL, + DONT_CARE, + CRC32 + } + + public struct sparseFileHeader + { + public uint magic; + public ushort majorVersion; + public ushort minorVersion; + public ushort fileHeaderSize; + public ushort chunkHeaderSize; + public uint blockSize; + public uint totalBlocks; + public uint totalChunks; + public uint imageChecksum; + } + + public struct sparseChunkHeader + { + public ushort chunkType; + public ushort reserved; + public uint chunkSize; + public uint totalSize; + } + + private static void VerifySparseFileHeader(sparseFileHeader fileHeader) + { + if (fileHeader.magic != sparse_magic) throw new Exception("Invalid sparse magic"); + if (fileHeader.majorVersion > 1) throw new Exception("Sparse format too new"); + if (fileHeader.fileHeaderSize != 28 && fileHeader.majorVersion == 1) throw new Exception("Invalid file header size"); + if (fileHeader.chunkHeaderSize != 12 && fileHeader.majorVersion == 1) throw new Exception("Invalid chunk header size"); + if (fileHeader.blockSize % 4 != 0) throw new Exception("Invalid block size"); + } + + public static sparseFileHeader ReadSparseFileHeader(byte[] buffer) + { + sparseFileHeader fileHeader = new(); + + using (var ms = new MemoryStream(buffer)) + using (var br = new BinaryReader(ms)) + { + fileHeader.magic = br.ReadUInt32(); + fileHeader.majorVersion = br.ReadUInt16(); + fileHeader.minorVersion = br.ReadUInt16(); + fileHeader.fileHeaderSize = br.ReadUInt16(); + fileHeader.chunkHeaderSize = br.ReadUInt16(); + fileHeader.blockSize = br.ReadUInt32(); + fileHeader.totalBlocks = br.ReadUInt32(); + fileHeader.totalChunks = br.ReadUInt32(); + fileHeader.imageChecksum = br.ReadUInt32(); + } + + VerifySparseFileHeader(fileHeader); + + return fileHeader; + } + + public static sparseChunkHeader ReadSparseChunkHeader(byte[] buffer) + { + sparseChunkHeader chunk_header = new(); + + using (var ms = new MemoryStream(buffer)) + using (var br = new BinaryReader(ms)) + { + chunk_header.chunkType = br.ReadUInt16(); + chunk_header.reserved = br.ReadUInt16(); + chunk_header.chunkSize = br.ReadUInt32(); + chunk_header.totalSize = br.ReadUInt32(); + } + return chunk_header; + } + + public static byte[] HeaderToBytes(sparseFileHeader fileHeader) + { + using (var ms = new MemoryStream()) + using (var bw = new BinaryWriter(ms)) + { + bw.Write(fileHeader.magic); + bw.Write(fileHeader.majorVersion); + bw.Write(fileHeader.minorVersion); + bw.Write(fileHeader.fileHeaderSize); + bw.Write(fileHeader.chunkHeaderSize); + bw.Write(fileHeader.blockSize); + bw.Write(fileHeader.totalBlocks); + bw.Write(fileHeader.totalChunks); + bw.Write(fileHeader.imageChecksum); + + return ms.ToArray(); + } + } + + public static byte[] ChunkHeaderToBytes(sparseChunkHeader chunk) + { + if (chunk.chunkType != (uint)chunkTypes.DONT_CARE) throw new Exception("Only DONT_CARE chunks can be generated in this lib."); + + using (var ms = new MemoryStream()) + using (var bw = new BinaryWriter(ms)) + { + bw.Write(chunk.chunkType); + bw.Write(chunk.reserved); + bw.Write(chunk.chunkSize); + bw.Write(chunk.totalSize); + + return ms.ToArray(); + } + } + } +} From cbd3359bb9ac0912e53d8a5d4e4b49cf66bd87c5 Mon Sep 17 00:00:00 2001 From: Umer Uddin Date: Sat, 13 Dec 2025 14:27:27 +0000 Subject: [PATCH 2/2] Add sparse splitting support to flashing command Splits files based on sparse chunks if the file is too big to flash in one go. Non-sparse files are not supported as of right now. Signed-off-by: Umer Uddin --- FastBootCommands.cs | 186 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/FastBootCommands.cs b/FastBootCommands.cs index 133ef44..6a08376 100644 --- a/FastBootCommands.cs +++ b/FastBootCommands.cs @@ -21,9 +21,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography.Xml; +using static FastBoot.Sparse; namespace FastBoot { @@ -264,6 +267,189 @@ public static bool RebootBootloader(this FastBootTransport fastBootTransport) public static bool FlashPartition(this FastBootTransport fastBootTransport, string partition, Stream stream) { FastBootStatus status; + uint maxDownloadSize = 0; + string deviceResponse; + + if(!GetVariable(fastBootTransport, "max-download-size", out deviceResponse)) return false; + maxDownloadSize = Convert.ToUInt32(deviceResponse); + + if (stream.Length > maxDownloadSize) + { + byte[] image_magic = new byte[4]; + + try + { + stream.Read(image_magic, 0, 4); + if(BitConverter.ToUInt32(image_magic) == Sparse.sparse_magic) + { + byte[] sparseFileHeaderBuffer = new byte[28]; + byte[] sparseChunkHeaderBuffer = new byte[12]; + sparseFileHeader fileHeader; + + List chunks = new(); + uint blockPosition = 0; + int availableSpace; + + stream.Position = 0; + + stream.ReadExactly(sparseFileHeaderBuffer, 0, 28); + fileHeader = ReadSparseFileHeader(sparseFileHeaderBuffer); + availableSpace = (int)maxDownloadSize - (int)fileHeader.fileHeaderSize; + + for (int i = 0; i < fileHeader.totalChunks; i++) + { + stream.ReadExactly(sparseChunkHeaderBuffer, 0, 12); + sparseChunkHeader chunkHeader = ReadSparseChunkHeader(sparseChunkHeaderBuffer); + + if (chunkHeader.totalSize > availableSpace && chunks.Count > 0) + { + sparseFileHeader newFileHeader = fileHeader; + int offset = fileHeader.fileHeaderSize; + + uint padding_blocks = fileHeader.totalBlocks - blockPosition; + if (padding_blocks > 0) + { + sparseChunkHeader dummyChunk = new sparseChunkHeader + { + chunkType = (ushort)chunkTypes.DONT_CARE, + reserved = 0, + chunkSize = padding_blocks, + totalSize = fileHeader.chunkHeaderSize + }; + chunks.Add(ChunkHeaderToBytes(dummyChunk)); + } + + newFileHeader.totalChunks = (uint)chunks.Count; + + byte[] fileBuffer = new byte[HeaderToBytes(newFileHeader).Length + chunks.Sum(chunk => chunk.Length)]; + Buffer.BlockCopy(HeaderToBytes(newFileHeader), 0, fileBuffer, 0, 28); + + foreach (byte[] chunk in chunks) + { + Buffer.BlockCopy(chunk, 0, fileBuffer, offset, chunk.Length); + offset += chunk.Length; + } + + using (MemoryStream ms = new MemoryStream(fileBuffer)) + { + try + { + (status, string _, byte[] _) = fastBootTransport.SendData(ms); + } + catch + { + return false; + } + + if (status != FastBootStatus.OKAY) + { + return false; + } + + try + { + (FastBootStatus status, string response, byte[] rawResponse)[] responses = fastBootTransport.SendCommand($"flash:{partition}"); + (status, string _, byte[] _) = responses.Last(); + } + catch + { + return false; + } + + if (status != FastBootStatus.OKAY) + { + return false; + } + } + + chunks.Clear(); + availableSpace = (int)maxDownloadSize - (int)fileHeader.fileHeaderSize; + + if (blockPosition > 0) + { + sparseChunkHeader dummyChunk = new sparseChunkHeader + { + chunkType = (ushort)chunkTypes.DONT_CARE, + reserved = 0, + chunkSize = blockPosition, + totalSize = fileHeader.chunkHeaderSize + }; + chunks.Add(ChunkHeaderToBytes(dummyChunk)); + availableSpace -= fileHeader.chunkHeaderSize; + } + } + + byte[] chunk_data = new byte[chunkHeader.totalSize]; + Buffer.BlockCopy(sparseChunkHeaderBuffer, 0, chunk_data, 0, 12); + + int payload = (int)chunkHeader.totalSize - 12; + stream.ReadExactly(chunk_data, 12, payload); + + chunks.Add(chunk_data); + blockPosition += chunkHeader.chunkSize; + availableSpace -= (int)chunkHeader.totalSize; + } + + if (chunks.Count > 0) + { + sparseFileHeader newFileHeader = fileHeader; + int offset = fileHeader.fileHeaderSize; + + newFileHeader.totalChunks = (uint)chunks.Count; + + byte[] fileBuffer = new byte[HeaderToBytes(newFileHeader).Length + chunks.Sum(chunk => chunk.Length)]; + Buffer.BlockCopy(HeaderToBytes(newFileHeader), 0, fileBuffer, 0, 28); + + foreach (byte[] chunk in chunks) + { + Buffer.BlockCopy(chunk, 0, fileBuffer, offset, chunk.Length); + offset += chunk.Length; + } + + using (MemoryStream ms = new MemoryStream(fileBuffer)) + { + try + { + (status, string _, byte[] _) = fastBootTransport.SendData(ms); + } + catch + { + return false; + } + + if (status != FastBootStatus.OKAY) + { + return false; + } + + try + { + (FastBootStatus status, string response, byte[] rawResponse)[] responses = fastBootTransport.SendCommand($"flash:{partition}"); + (status, string _, byte[] _) = responses.Last(); + } + catch + { + return false; + } + + if (status != FastBootStatus.OKAY) + { + return false; + } + } + } + return true; + } + else + { + return false; // TODO: Handle non-sparse but large images (most likely just converting them into raw chunks and sending them off). + } + } + catch + { + return false; + } + } try {