From 1d580bad34100a9ac7d20e28030931edfa1bab11 Mon Sep 17 00:00:00 2001 From: Mohammad Abdul Sahil <127765312+abdulsaheel@users.noreply.github.com> Date: Fri, 13 Nov 2026 01:06:55 +0530 Subject: [PATCH 1/2] add image update payload builders (gen5) --- lib/openstrap_protocol.dart | 1 + lib/src/firmware_update.dart | 44 ++++++++++++++++++++++++++++++++++ test/firmware_update_test.dart | 23 ++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 lib/src/firmware_update.dart create mode 100644 test/firmware_update_test.dart diff --git a/lib/openstrap_protocol.dart b/lib/openstrap_protocol.dart index 20fb05c..226428c 100644 --- a/lib/openstrap_protocol.dart +++ b/lib/openstrap_protocol.dart @@ -100,6 +100,7 @@ export 'src/realtime96.dart' // Source 2 — CRC, constants, framing, commands. export 'src/crc.dart' show crc8, crc32, crc16Modbus; export 'src/constants.dart'; +export 'src/firmware_update.dart'; export 'src/framing.dart' show Frame, pad4, buildFrame, parseFrame, FrameReassembler; export 'src/commands.dart' diff --git a/lib/src/firmware_update.dart b/lib/src/firmware_update.dart new file mode 100644 index 0000000..3440e61 --- /dev/null +++ b/lib/src/firmware_update.dart @@ -0,0 +1,44 @@ +// Device image update commands (gen5 only). Pure payload builders: nothing +// here sends anything, and none of these opcodes may leave the host without +// the caller's explicit allowDangerous opt-in (all four are destructive). +// +// 0x8E start load [0x01] +// 0x8F load data [0x01][offset u32 LE][len u8][data] +// 0x53 verify image [0x01] +// 0x90 process image [0x01] — commit; cannot be undone from the host. +// +// A status of 1 on 0x90 only means the staged bytes were accepted and +// checksummed. It is not proof the image is now running. +import 'dart:typed_data'; + +class FwUpdate { + static const int startLoad = 0x8E; + static const int loadData = 0x8F; + static const int verifyImage = 0x53; + static const int processImage = 0x90; + + /// Length field of [loadData] is one byte. + static const int maxChunk = 255; + + /// Bytes per [loadData]. Small enough that the framed write fits a typical + /// negotiated ATT payload (8 header + padded 9+128 inner + 4 crc = 152). + static const int chunk = 128; + + static const List rev1 = [0x01]; + + static Uint8List loadDataPayload(int offset, List data) { + if (data.isEmpty || data.length > maxChunk) { + throw ArgumentError('chunk must be 1..$maxChunk bytes, got ${data.length}'); + } + final b = BytesBuilder() + ..addByte(0x01) + ..add((ByteData(4)..setUint32(0, offset, Endian.little)) + .buffer + .asUint8List()) + ..addByte(data.length) + ..add(data); + return b.toBytes(); + } + + static int chunkCount(int imageLen) => (imageLen + chunk - 1) ~/ chunk; +} diff --git a/test/firmware_update_test.dart b/test/firmware_update_test.dart new file mode 100644 index 0000000..9d607dc --- /dev/null +++ b/test/firmware_update_test.dart @@ -0,0 +1,23 @@ +import 'package:openstrap_protocol/openstrap_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + test('load data payload layout', () { + final p = FwUpdate.loadDataPayload(0x01020304, [9, 8, 7]); + expect(p, [0x01, 0x04, 0x03, 0x02, 0x01, 3, 9, 8, 7]); + }); + test('rejects empty and oversize chunks', () { + expect(() => FwUpdate.loadDataPayload(0, const []), throwsArgumentError); + expect(() => FwUpdate.loadDataPayload(0, List.filled(256, 0)), + throwsArgumentError); + }); + test('chunk count rounds up', () { + expect(FwUpdate.chunkCount(128), 1); + expect(FwUpdate.chunkCount(129), 2); + }); + test('start, load and process are blocked by the generic guard', () { + for (final op in [FwUpdate.startLoad, FwUpdate.loadData, FwUpdate.processImage]) { + expect(dangerousCmds.contains(op), isTrue); + } + }); +} From 78602eb6714e7533b32d16c0010a3486ed3aaedb Mon Sep 17 00:00:00 2001 From: Mohammad Abdul Sahil <127765312+abdulsaheel@users.noreply.github.com> Date: Fri, 13 Nov 2026 01:12:45 +0530 Subject: [PATCH 2/2] image update: reject bad offsets and empty lengths, fix the opcode comment --- lib/src/firmware_update.dart | 11 +++++++++-- test/firmware_update_test.dart | 8 ++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/src/firmware_update.dart b/lib/src/firmware_update.dart index 3440e61..81ceb3e 100644 --- a/lib/src/firmware_update.dart +++ b/lib/src/firmware_update.dart @@ -1,6 +1,7 @@ // Device image update commands (gen5 only). Pure payload builders: nothing // here sends anything, and none of these opcodes may leave the host without -// the caller's explicit allowDangerous opt-in (all four are destructive). +// the caller's explicit allowDangerous opt-in (0x8E, 0x8F and 0x90 are +// destructive; 0x53 only asks the strap to check what is staged). // // 0x8E start load [0x01] // 0x8F load data [0x01][offset u32 LE][len u8][data] @@ -30,6 +31,9 @@ class FwUpdate { if (data.isEmpty || data.length > maxChunk) { throw ArgumentError('chunk must be 1..$maxChunk bytes, got ${data.length}'); } + if (offset < 0 || offset > 0xFFFFFFFF) { + throw ArgumentError('offset out of u32 range: $offset'); + } final b = BytesBuilder() ..addByte(0x01) ..add((ByteData(4)..setUint32(0, offset, Endian.little)) @@ -40,5 +44,8 @@ class FwUpdate { return b.toBytes(); } - static int chunkCount(int imageLen) => (imageLen + chunk - 1) ~/ chunk; + static int chunkCount(int imageLen) { + if (imageLen <= 0) throw ArgumentError('image length must be > 0, got $imageLen'); + return (imageLen + chunk - 1) ~/ chunk; + } } diff --git a/test/firmware_update_test.dart b/test/firmware_update_test.dart index 9d607dc..bb471c5 100644 --- a/test/firmware_update_test.dart +++ b/test/firmware_update_test.dart @@ -11,6 +11,14 @@ void main() { expect(() => FwUpdate.loadDataPayload(0, List.filled(256, 0)), throwsArgumentError); }); + test('rejects offsets outside u32', () { + expect(() => FwUpdate.loadDataPayload(-1, [1]), throwsArgumentError); + expect(() => FwUpdate.loadDataPayload(0x100000000, [1]), throwsArgumentError); + }); + test('chunk count rejects empty and negative lengths', () { + expect(() => FwUpdate.chunkCount(0), throwsArgumentError); + expect(() => FwUpdate.chunkCount(-5), throwsArgumentError); + }); test('chunk count rounds up', () { expect(FwUpdate.chunkCount(128), 1); expect(FwUpdate.chunkCount(129), 2);