Skip to content

Commit 9daaa5c

Browse files
committed
tools/mpremote: Add streaming hash verification to file transfers.
Adds verify_hash parameter to fs_readfile and fs_writefile that computes SHA256 hash during transfer in both directions. Enabled by default when check_hash is active, verifies transmission integrity without requiring flash re-read. Signed-off-by: Andrew Leech <[email protected]>
1 parent 27544a2 commit 9daaa5c

File tree

2 files changed

+56
-11
lines changed

2 files changed

+56
-11
lines changed

tools/mpremote/mpremote/commands.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ def do_filesystem_cp(state, src, dest, multiple, check_hash=False):
155155
# Download the contents of source.
156156
try:
157157
if src.startswith(":"):
158-
data = state.transport.fs_readfile(src[1:], progress_callback=show_progress_bar)
158+
data = state.transport.fs_readfile(
159+
src[1:], progress_callback=show_progress_bar, verify_hash=check_hash
160+
)
159161
filename = _remote_path_basename(src[1:])
160162
else:
161163
with open(src, "rb") as f:
@@ -183,8 +185,10 @@ def do_filesystem_cp(state, src, dest, multiple, check_hash=False):
183185
except OSError:
184186
pass
185187

186-
# Write to remote.
187-
state.transport.fs_writefile(dest[1:], data, progress_callback=show_progress_bar)
188+
# Write to remote with hash verification if check_hash is enabled.
189+
state.transport.fs_writefile(
190+
dest[1:], data, progress_callback=show_progress_bar, verify_hash=check_hash
191+
)
188192
else:
189193
# If the destination path is just the directory, then add the source filename.
190194
if dest_isdir:

tools/mpremote/mpremote/transport.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,42 +130,83 @@ def fs_printfile(self, src, chunk_size=256):
130130
except TransportExecError as e:
131131
raise _convert_filesystem_error(e, src) from None
132132

133-
def fs_readfile(self, src, chunk_size=256, progress_callback=None):
133+
def fs_readfile(self, src, chunk_size=256, progress_callback=None, verify_hash=False):
134134
if progress_callback:
135135
src_size = self.fs_stat(src).st_size
136136

137137
contents = bytearray()
138138

139139
try:
140-
self.exec("f=open('%s','rb')\nr=f.read" % src)
140+
if verify_hash:
141+
# Initialize hash on device along with file read
142+
self.exec(
143+
"import hashlib\nh=hashlib.sha256()\nf=open('%s','rb')\nr=f.read\nu=h.update"
144+
% src
145+
)
146+
else:
147+
self.exec("f=open('%s','rb')\nr=f.read" % src)
148+
141149
while True:
142150
chunk = self.eval("r({})".format(chunk_size))
143151
if not chunk:
144152
break
153+
if verify_hash:
154+
# Update hash with chunk
155+
self.exec("u(" + repr(chunk) + ")")
145156
contents.extend(chunk)
146157
if progress_callback:
147158
progress_callback(len(contents), src_size)
148159
self.exec("f.close()")
160+
161+
if verify_hash:
162+
# Get final hash from device
163+
remote_hash = self.eval("h.digest()")
164+
# Calculate local hash
165+
local_hash = hashlib.sha256(contents).digest()
166+
if remote_hash != local_hash:
167+
raise TransportError("file transfer verification failed for '%s'" % src)
149168
except TransportExecError as e:
150169
raise _convert_filesystem_error(e, src) from None
151170

152171
return contents
153172

154-
def fs_writefile(self, dest, data, chunk_size=256, progress_callback=None):
173+
def fs_writefile(self, dest, data, chunk_size=256, progress_callback=None, verify_hash=False):
155174
if progress_callback:
156175
src_size = len(data)
157176
written = 0
158177

159178
try:
160-
self.exec("f=open('%s','wb')\nw=f.write" % dest)
161-
while data:
162-
chunk = data[:chunk_size]
163-
self.exec("w(" + repr(chunk) + ")")
164-
data = data[len(chunk) :]
179+
if verify_hash:
180+
# Calculate source hash
181+
source_hash = hashlib.sha256(data).digest()
182+
# Initialize hash on device along with file write
183+
self.exec(
184+
"import hashlib\nh=hashlib.sha256()\nf=open('%s','wb')\nw=f.write\nu=h.update"
185+
% dest
186+
)
187+
else:
188+
self.exec("f=open('%s','wb')\nw=f.write" % dest)
189+
190+
data_remaining = data
191+
while data_remaining:
192+
chunk = data_remaining[:chunk_size]
193+
if verify_hash:
194+
# Write chunk and update hash in one call
195+
self.exec("d=" + repr(chunk) + "\nw(d)\nu(d)")
196+
else:
197+
self.exec("w(" + repr(chunk) + ")")
198+
data_remaining = data_remaining[len(chunk) :]
165199
if progress_callback:
166200
written += len(chunk)
167201
progress_callback(written, src_size)
202+
168203
self.exec("f.close()")
204+
205+
if verify_hash:
206+
# Get final hash from device
207+
remote_hash = self.eval("h.digest()")
208+
if remote_hash != source_hash:
209+
raise TransportError("file transfer verification failed for '%s'" % dest)
169210
except TransportExecError as e:
170211
raise _convert_filesystem_error(e, dest) from None
171212

0 commit comments

Comments
 (0)