Skip to content

Commit a1477be

Browse files
authored
Merge pull request #143 from radrogow/add/repl
Add repl module and entrypoint
2 parents 97ea16b + c817a45 commit a1477be

6 files changed

Lines changed: 450 additions & 4 deletions

File tree

README.rst

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ littlefs-python comes bundled with a command-line tool, ``littlefs-python``, tha
103103
.. code:: console
104104
105105
$ littlefs-python --help
106-
usage: littlefs-python [-h] [--version] {create,extract,list} ...
106+
usage: littlefs-python [-h] [--version] {create,extract,list,repl} ...
107107
108108
Create, extract and inspect LittleFS filesystem images. Use one of the
109109
commands listed below, the '-h' / '--help' option can be used on each command
@@ -114,10 +114,11 @@ littlefs-python comes bundled with a command-line tool, ``littlefs-python``, tha
114114
--version show program's version number and exit
115115
116116
Available Commands:
117-
{create,extract,list}
117+
{create,extract,list,repl}
118118
create Create LittleFS image from file/directory contents.
119119
extract Extract LittleFS image contents to a directory.
120120
list List LittleFS image contents.
121+
repl Inspect an existing LittleFS image through an interactive shell.
121122
122123
To create a littlefs binary image:
123124

@@ -135,6 +136,17 @@ To extract the contents of a littlefs binary image:
135136
136137
$ littlefs-python extract lfs.bin output/ --block-size=4096
137138
139+
To inspect or debug an existing image without extracting it first you can start a
140+
simple REPL. It provides shell-like commands such as ``ls``, ``tree``, ``put``, ``get``
141+
and ``rm`` that operate directly on the image data:
142+
143+
.. code:: console
144+
145+
$ littlefs-python repl lfs.bin --block-size=4096
146+
Mounted remote littlefs.
147+
littlefs> ls
148+
README.rst
149+
138150
Development Setup
139151
=================
140152

src/littlefs/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"LittleFS",
3535
"LittleFSError",
3636
"UserContext",
37+
"UserContextFile",
3738
"UserContextWinDisk",
3839
"__LFS_DISK_VERSION__",
3940
"__LFS_VERSION__",
@@ -48,7 +49,7 @@
4849
# Package not installed
4950
pass
5051

51-
from .context import UserContext, UserContextWinDisk
52+
from .context import UserContext, UserContextFile, UserContextWinDisk
5253

5354
if TYPE_CHECKING:
5455
from .lfs import LFSStat

src/littlefs/__main__.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from littlefs import LittleFS, __version__
88
from littlefs.errors import LittleFSError
9+
from littlefs.repl import LittleFSRepl
10+
from littlefs.context import UserContextFile
911

1012
# Dictionary mapping suffixes to their size in bytes
1113
_suffix_map = {
@@ -110,7 +112,7 @@ def create(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
110112
compact_fs.fs_grow(args.block_count)
111113
data = compact_fs.context.buffer
112114
if not args.no_pad:
113-
data = data.ljust(args.fs_size, b"\xFF")
115+
data = data.ljust(args.fs_size, b"\xff")
114116
else:
115117
data = fs.context.buffer
116118

@@ -188,6 +190,47 @@ def extract(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
188190
return 0
189191

190192

193+
def repl(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
194+
"""Inspect an existing LittleFS image through an interactive shell."""
195+
196+
source: Path = args.source
197+
if not source.is_file():
198+
parser.error(f"Source image '{source}' does not exist.")
199+
200+
image_size = source.stat().st_size
201+
if not image_size or image_size % args.block_size:
202+
parser.error(
203+
f"Image size ({image_size} bytes) is not a multiple of the supplied block size ({args.block_size})."
204+
)
205+
206+
block_count = image_size // args.block_size
207+
if block_count == 0:
208+
parser.error("Image is smaller than a single block; cannot mount.")
209+
210+
context = UserContextFile(str(source))
211+
fs = LittleFS(
212+
context=context,
213+
block_size=args.block_size,
214+
block_count=block_count,
215+
name_max=args.name_max,
216+
mount=False,
217+
)
218+
219+
shell = LittleFSRepl(fs)
220+
try:
221+
try:
222+
shell.do_mount()
223+
except LittleFSError as exc:
224+
parser.error(f"Failed to mount '{source}': {exc}")
225+
shell.cmdloop()
226+
finally:
227+
if shell._mounted:
228+
with suppress(LittleFSError):
229+
fs.unmount()
230+
231+
return 0
232+
233+
191234
def get_parser():
192235
if sys.argv[0].endswith("__main__.py"):
193236
prog = f"python -m littlefs"
@@ -299,6 +342,19 @@ def add_command(handler, name="", help=""):
299342
help="LittleFS block size.",
300343
)
301344

345+
parser_repl = add_command(repl)
346+
parser_repl.add_argument(
347+
"source",
348+
type=Path,
349+
help="Source LittleFS filesystem binary.",
350+
)
351+
parser_repl.add_argument(
352+
"--block-size",
353+
type=size_parser,
354+
required=True,
355+
help="LittleFS block size.",
356+
)
357+
302358
return parser
303359

304360

src/littlefs/context.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import typing
33
import ctypes
4+
import os
45

56
if typing.TYPE_CHECKING:
67
from .lfs import LFSConfig
@@ -78,6 +79,60 @@ def sync(self, cfg: "LFSConfig") -> int:
7879
return 0
7980

8081

82+
class UserContextFile(UserContext):
83+
"""File-backed context using the standard library"""
84+
85+
def __init__(self, file_path: str, *, create: bool = False) -> None:
86+
mode = "r+b"
87+
if not os.path.exists(file_path):
88+
if not create:
89+
raise FileNotFoundError(f"Context file '{file_path}' does not exist")
90+
mode = "w+b"
91+
92+
self._path = file_path
93+
self._fh = open(file_path, mode)
94+
95+
def read(self, cfg: "LFSConfig", block: int, off: int, size: int) -> bytearray:
96+
logging.getLogger(__name__).debug("LFS Read : Block: %d, Offset: %d, Size=%d" % (block, off, size))
97+
start = block * cfg.block_size + off
98+
self._fh.seek(start)
99+
data = self._fh.read(size)
100+
101+
if len(data) < size:
102+
data += b"\xff" * (size - len(data))
103+
104+
return bytearray(data)
105+
106+
def prog(self, cfg: "LFSConfig", block: int, off: int, data: bytes) -> int:
107+
logging.getLogger(__name__).debug("LFS Prog : Block: %d, Offset: %d, Data=%r" % (block, off, data))
108+
start = block * cfg.block_size + off
109+
self._fh.seek(start)
110+
self._fh.write(data)
111+
return 0
112+
113+
def erase(self, cfg: "LFSConfig", block: int) -> int:
114+
logging.getLogger(__name__).debug("LFS Erase: Block: %d" % block)
115+
start = block * cfg.block_size
116+
self._fh.seek(start)
117+
self._fh.write(b"\xff" * cfg.block_size)
118+
return 0
119+
120+
def sync(self, cfg: "LFSConfig") -> int:
121+
self._fh.flush()
122+
os.fsync(self._fh.fileno())
123+
return 0
124+
125+
def close(self) -> None:
126+
if not self._fh.closed:
127+
self._fh.close()
128+
129+
def __del__(self):
130+
try:
131+
self.close()
132+
except Exception:
133+
pass
134+
135+
81136
try:
82137
import win32file
83138
except ImportError:

0 commit comments

Comments
 (0)