Skip to content

Commit b52d778

Browse files
committed
feat: add public GetSecurityInfo and FlashMD5 API methods
1 parent 3a71cb3 commit b52d778

5 files changed

Lines changed: 207 additions & 3 deletions

File tree

pkg/espflasher/flasher.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ type Flasher struct {
119119
chip *chipDef
120120
opts *FlasherOptions
121121
portStr string
122+
secInfo []byte // cached security info from ROM (GET_SECURITY_INFO opcode 0x14)
122123
}
123124

124125
// New creates a new Flasher connected to the given serial port.
@@ -665,8 +666,13 @@ func (f *Flasher) EraseFlash() error {
665666
}
666667

667668
// EraseRegion erases a region of flash memory.
669+
// Requires the stub loader to be running.
668670
// Both offset and size must be aligned to the flash sector size (4096 bytes).
669671
func (f *Flasher) EraseRegion(offset, size uint32) error {
672+
if !f.conn.isStub() {
673+
return &UnsupportedCommandError{Command: "erase region (requires stub)"}
674+
}
675+
670676
if offset%flashSectorSize != 0 {
671677
return fmt.Errorf("offset 0x%X is not aligned to sector size 0x%X", offset, flashSectorSize)
672678
}
@@ -688,6 +694,29 @@ func (f *Flasher) WriteRegister(addr, value uint32) error {
688694
return f.conn.writeReg(addr, value, 0xFFFFFFFF, 0)
689695
}
690696

697+
// GetSecurityInfo returns security-related information from the device.
698+
func (f *Flasher) GetSecurityInfo() (*SecurityInfo, error) {
699+
return f.readSecurityInfo()
700+
}
701+
702+
// FlashMD5 returns the MD5 hash of a flash region.
703+
// Requires the stub loader to be running.
704+
func (f *Flasher) FlashMD5(offset, size uint32) (string, error) {
705+
if !f.conn.isStub() {
706+
return "", &UnsupportedCommandError{Command: "flash MD5 (requires stub)"}
707+
}
708+
709+
if err := f.attachFlash(); err != nil {
710+
return "", err
711+
}
712+
713+
result, err := f.conn.flashMD5(offset, size)
714+
if err != nil {
715+
return "", err
716+
}
717+
return hex.EncodeToString(result), nil
718+
}
719+
691720
// Reset performs a hard reset of the device, causing it to run user code.
692721
func (f *Flasher) Reset() {
693722
if f.conn.isStub() {

pkg/espflasher/flasher_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,46 @@ func TestFlashSizeFromJEDECMatchesChipSizes(t *testing.T) {
433433
}
434434
}
435435
}
436+
437+
func TestFlashMD5RequiresStub(t *testing.T) {
438+
mock := &mockConnection{}
439+
mock.stubMode = false // ROM mode
440+
f := &Flasher{conn: mock, chip: chipDefs[ChipESP32]}
441+
_, err := f.FlashMD5(0, 1024)
442+
if err == nil {
443+
t.Fatal("expected error when stub is not running")
444+
}
445+
if ue, ok := err.(*UnsupportedCommandError); !ok {
446+
t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err)
447+
} else if ue.Command != "flash MD5 (requires stub)" {
448+
t.Errorf("unexpected error message: %s", ue.Command)
449+
}
450+
}
451+
452+
func TestGetSecurityInfo(t *testing.T) {
453+
secInfo := make([]byte, 20)
454+
binary.LittleEndian.PutUint32(secInfo[0:4], 0x05)
455+
secInfo[4] = 0x02
456+
binary.LittleEndian.PutUint32(secInfo[12:16], 0x1234)
457+
binary.LittleEndian.PutUint32(secInfo[16:20], 0x0200)
458+
459+
mock := &mockConnection{}
460+
mock.securityInfoFunc = func() ([]byte, error) {
461+
return secInfo, nil
462+
}
463+
f := &Flasher{conn: mock}
464+
info, err := f.GetSecurityInfo()
465+
if err != nil {
466+
t.Fatalf("GetSecurityInfo failed: %v", err)
467+
}
468+
if info == nil {
469+
t.Fatal("expected non-nil SecurityInfo")
470+
}
471+
if info.ChipID == nil || *info.ChipID != 0x1234 {
472+
if info.ChipID != nil {
473+
t.Errorf("unexpected chip ID: got 0x%X, want 0x1234", *info.ChipID)
474+
} else {
475+
t.Error("unexpected nil ChipID")
476+
}
477+
}
478+
}

pkg/espflasher/protocol.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ func (c *conn) writeReg(addr, value, mask, delayUS uint32) error {
284284
// securityInfo reads security-related information from the device.
285285
// Try with 20 bytes first (most chips), fallback to 12 bytes (ESP32-S2).
286286
func (c *conn) securityInfo() ([]byte, error) {
287+
c.flushInput()
287288
data := make([]byte, 20)
288289

289290
result, err := c.checkCommand("get security info", cmdSecurityInfoReg, data, 0, defaultTimeout, 20)

pkg/espflasher/security_info.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,19 @@ type ParsedFlags struct {
3030
}
3131

3232
func (f *Flasher) readSecurityInfo() (*SecurityInfo, error) {
33-
res, err := f.conn.securityInfo()
34-
if err != nil {
35-
return nil, err
33+
var res []byte
34+
var err error
35+
36+
// Use cached security info if available (from ROM before stub was loaded)
37+
if len(f.secInfo) > 0 {
38+
res = f.secInfo
39+
} else {
40+
res, err = f.conn.securityInfo()
41+
if err != nil {
42+
return nil, err
43+
}
44+
// Cache the raw bytes for future calls
45+
f.secInfo = res
3646
}
3747

3848
var si SecurityInfo

pkg/espflasher/security_info_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,124 @@ func TestReadSecurityInfo(t *testing.T) {
202202
})
203203
}
204204
}
205+
206+
func TestSecurityInfoFlushInput(t *testing.T) {
207+
// Test that securityInfo clears any stray input before querying.
208+
// This verifies the fix for the ESP32-S3 issue where SLIP frame delimiters
209+
// in the input buffer corrupted the response (command 0x14 failed: status=0xC0).
210+
buf20 := make([]byte, 20)
211+
binary.LittleEndian.PutUint32(buf20[0:4], 0x05)
212+
buf20[4] = 0x03
213+
binary.LittleEndian.PutUint32(buf20[12:16], 0x0009)
214+
215+
mc := &mockConnection{
216+
securityInfoFunc: func() ([]byte, error) {
217+
return buf20, nil
218+
},
219+
}
220+
f := &Flasher{conn: mc}
221+
222+
si, err := f.readSecurityInfo()
223+
if err != nil {
224+
t.Fatalf("unexpected error: %v", err)
225+
}
226+
227+
if si.Flags != 0x05 {
228+
t.Errorf("Flags = 0x%x, want 0x05", si.Flags)
229+
}
230+
231+
if si == nil {
232+
t.Error("expected SecurityInfo to be populated")
233+
}
234+
}
235+
236+
func TestSecurityInfoCaching(t *testing.T) {
237+
// Test that security info is cached from the first call (ROM before stub loads)
238+
// and subsequent calls return the cached bytes without re-issuing the command.
239+
buf20 := make([]byte, 20)
240+
binary.LittleEndian.PutUint32(buf20[0:4], 0x05) // flags
241+
buf20[4] = 0x03 // FlashCryptCnt
242+
binary.LittleEndian.PutUint32(buf20[12:16], 0x0009) // ChipID
243+
244+
callCount := 0
245+
mc := &mockConnection{
246+
securityInfoFunc: func() ([]byte, error) {
247+
callCount++
248+
return buf20, nil
249+
},
250+
}
251+
f := &Flasher{conn: mc}
252+
253+
// First call should invoke the connection
254+
si1, err := f.readSecurityInfo()
255+
if err != nil {
256+
t.Fatalf("first call: unexpected error: %v", err)
257+
}
258+
if si1.Flags != 0x05 {
259+
t.Errorf("first call: Flags = 0x%x, want 0x05", si1.Flags)
260+
}
261+
if callCount != 1 {
262+
t.Errorf("first call: expected 1 connection call, got %d", callCount)
263+
}
264+
265+
// Second call should use the cached bytes without calling conn.securityInfo()
266+
si2, err := f.readSecurityInfo()
267+
if err != nil {
268+
t.Fatalf("second call: unexpected error: %v", err)
269+
}
270+
if si2.Flags != 0x05 {
271+
t.Errorf("second call: Flags = 0x%x, want 0x05", si2.Flags)
272+
}
273+
if callCount != 1 {
274+
t.Errorf("second call: expected 1 total connection call, got %d", callCount)
275+
}
276+
277+
// Results should be identical
278+
if si1.Flags != si2.Flags || si1.FlashCryptCnt != si2.FlashCryptCnt {
279+
t.Error("cached result differs from initial result")
280+
}
281+
}
282+
283+
func TestSecurityInfoCachingWithStubFailure(t *testing.T) {
284+
// Test that when security info is cached from ROM, a stub failure (0xC0)
285+
// on a second call to GetSecurityInfo() returns the cached data instead.
286+
buf20 := make([]byte, 20)
287+
binary.LittleEndian.PutUint32(buf20[0:4], 0x05) // flags
288+
buf20[4] = 0x03 // FlashCryptCnt
289+
binary.LittleEndian.PutUint32(buf20[12:16], 0x0009) // ChipID
290+
291+
callCount := 0
292+
mc := &mockConnection{
293+
securityInfoFunc: func() ([]byte, error) {
294+
callCount++
295+
if callCount == 1 {
296+
// First call (ROM) succeeds
297+
return buf20, nil
298+
}
299+
// Second call (stub) would fail, but it won't be called
300+
return nil, errors.New("stub does not support command 0x14")
301+
},
302+
}
303+
f := &Flasher{conn: mc}
304+
305+
// First call succeeds and caches the data
306+
si1, err := f.readSecurityInfo()
307+
if err != nil {
308+
t.Fatalf("first call: unexpected error: %v", err)
309+
}
310+
if si1.Flags != 0x05 {
311+
t.Errorf("first call: Flags = 0x%x, want 0x05", si1.Flags)
312+
}
313+
314+
// Second call returns cached data without hitting the connection
315+
si2, err := f.readSecurityInfo()
316+
if err != nil {
317+
t.Fatalf("second call: unexpected error: %v", err)
318+
}
319+
if si2.Flags != 0x05 {
320+
t.Errorf("second call: Flags = 0x%x, want 0x05", si2.Flags)
321+
}
322+
if callCount != 1 {
323+
t.Errorf("expected only 1 connection call (second should use cache), got %d", callCount)
324+
}
325+
}

0 commit comments

Comments
 (0)