littlefs-python 0.17.0__tar.gz → 0.18.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/.github/workflows/deploy.yml +3 -3
  2. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/PKG-INFO +1 -1
  3. littlefs_python-0.18.0/requirements.txt +3 -0
  4. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs/__init__.py +41 -17
  5. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs/__main__.py +115 -66
  6. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs/context.py +12 -4
  7. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs/lfs.c +3855 -4171
  8. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs/lfs.pyi +12 -10
  9. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs/lfs.pyx +40 -25
  10. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs/repl.py +5 -1
  11. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs_python.egg-info/PKG-INFO +1 -1
  12. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs_python.egg-info/SOURCES.txt +5 -0
  13. littlefs_python-0.18.0/test/cli/test_create_and_extract.py +193 -0
  14. littlefs_python-0.18.0/test/cli/test_create_and_repl.py +99 -0
  15. littlefs_python-0.18.0/test/cli/test_walk_all.py +60 -0
  16. littlefs_python-0.18.0/test/test_unicode_filenames.py +101 -0
  17. littlefs_python-0.18.0/test/test_windisk_context.py +60 -0
  18. littlefs_python-0.17.0/requirements.txt +0 -2
  19. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/.gitattributes +0 -0
  20. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/.github/dependabot.yml +0 -0
  21. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/.github/workflows/run-tests.yml +0 -0
  22. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/.gitignore +0 -0
  23. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/.gitmodules +0 -0
  24. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/.pre-commit-config.yaml +0 -0
  25. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/.readthedocs.yml +0 -0
  26. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/LICENSE +0 -0
  27. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/MANIFEST.in +0 -0
  28. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/README.rst +0 -0
  29. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/ci/build-wheels.sh +0 -0
  30. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/ci/download_release_files.py +0 -0
  31. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/docs/Makefile +0 -0
  32. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/docs/api/index.rst +0 -0
  33. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/docs/cli.rst +0 -0
  34. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/docs/conf.py +0 -0
  35. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/docs/doc8.ini +0 -0
  36. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/docs/examples/index.rst +0 -0
  37. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/docs/index.rst +0 -0
  38. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/docs/make.bat +0 -0
  39. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/docs/requirements.txt +0 -0
  40. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/docs/usage.rst +0 -0
  41. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/examples/mkfsimg.py +0 -0
  42. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/examples/walk.py +0 -0
  43. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/.git +0 -0
  44. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/.gitattributes +0 -0
  45. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/.github/workflows/post-release.yml +0 -0
  46. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/.github/workflows/release.yml +0 -0
  47. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/.github/workflows/status.yml +0 -0
  48. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/.github/workflows/test.yml +0 -0
  49. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/.gitignore +0 -0
  50. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/DESIGN.md +0 -0
  51. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/LICENSE.md +0 -0
  52. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/Makefile +0 -0
  53. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/README.md +0 -0
  54. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/SPEC.md +0 -0
  55. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/bd/lfs_emubd.c +0 -0
  56. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/bd/lfs_emubd.h +0 -0
  57. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/bd/lfs_filebd.c +0 -0
  58. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/bd/lfs_filebd.h +0 -0
  59. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/bd/lfs_rambd.c +0 -0
  60. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/bd/lfs_rambd.h +0 -0
  61. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/benches/bench_dir.toml +0 -0
  62. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/benches/bench_file.toml +0 -0
  63. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/benches/bench_superblock.toml +0 -0
  64. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/lfs.c +0 -0
  65. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/lfs.h +0 -0
  66. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/lfs_util.c +0 -0
  67. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/lfs_util.h +0 -0
  68. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/runners/bench_runner.c +0 -0
  69. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/runners/bench_runner.h +0 -0
  70. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/runners/test_runner.c +0 -0
  71. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/runners/test_runner.h +0 -0
  72. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/bench.py +0 -0
  73. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/changeprefix.py +0 -0
  74. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/code.py +0 -0
  75. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/cov.py +0 -0
  76. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/data.py +0 -0
  77. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/perf.py +0 -0
  78. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/perfbd.py +0 -0
  79. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/plot.py +0 -0
  80. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/plotmpl.py +0 -0
  81. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/prettyasserts.py +0 -0
  82. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/readblock.py +0 -0
  83. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/readmdir.py +0 -0
  84. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/readtree.py +0 -0
  85. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/stack.py +0 -0
  86. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/structs.py +0 -0
  87. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/summary.py +0 -0
  88. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/tailpipe.py +0 -0
  89. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/teepipe.py +0 -0
  90. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/test.py +0 -0
  91. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/tracebd.py +0 -0
  92. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/scripts/watch.py +0 -0
  93. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_alloc.toml +0 -0
  94. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_attrs.toml +0 -0
  95. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_badblocks.toml +0 -0
  96. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_bd.toml +0 -0
  97. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_compat.toml +0 -0
  98. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_dirs.toml +0 -0
  99. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_entries.toml +0 -0
  100. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_evil.toml +0 -0
  101. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_exhaustion.toml +0 -0
  102. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_files.toml +0 -0
  103. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_interspersed.toml +0 -0
  104. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_move.toml +0 -0
  105. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_orphans.toml +0 -0
  106. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_paths.toml +0 -0
  107. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_powerloss.toml +0 -0
  108. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_relocations.toml +0 -0
  109. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_seek.toml +0 -0
  110. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_shrink.toml +0 -0
  111. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_superblocks.toml +0 -0
  112. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/littlefs/tests/test_truncate.toml +0 -0
  113. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/mypy.ini +0 -0
  114. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/pyproject.toml +0 -0
  115. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/setup.cfg +0 -0
  116. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/setup.py +0 -0
  117. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs/errors.py +0 -0
  118. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs/lfs.pxd +0 -0
  119. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs/py.typed +0 -0
  120. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs_python.egg-info/dependency_links.txt +0 -0
  121. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs_python.egg-info/entry_points.txt +0 -0
  122. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs_python.egg-info/not-zip-safe +0 -0
  123. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs_python.egg-info/requires.txt +0 -0
  124. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/src/littlefs_python.egg-info/top_level.txt +0 -0
  125. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/lfs/conftest.py +0 -0
  126. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/lfs/test_dir_functions.py +0 -0
  127. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/lfs/test_file_functions.py +0 -0
  128. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/lfs/test_fs_functions.py +0 -0
  129. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/test_attr.py +0 -0
  130. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/test_block_count.py +0 -0
  131. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/test_context.py +0 -0
  132. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/test_directories.py +0 -0
  133. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/test_files.py +0 -0
  134. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/test_grow.py +0 -0
  135. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/test_multiversion.py +0 -0
  136. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/test_name_max.py +0 -0
  137. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/test_remove_rename.py +0 -0
  138. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/test_version.py +0 -0
  139. {littlefs_python-0.17.0 → littlefs_python-0.18.0}/test/test_walk.py +0 -0
@@ -35,7 +35,7 @@ jobs:
35
35
  platforms: all
36
36
 
37
37
  - name: Build wheels
38
- uses: pypa/cibuildwheel@v3.3.0
38
+ uses: pypa/cibuildwheel@v3.4.1
39
39
 
40
40
  - uses: actions/upload-artifact@v6
41
41
  with:
@@ -57,7 +57,7 @@ jobs:
57
57
  fetch-depth: 0
58
58
 
59
59
  - name: Build wheels
60
- uses: pypa/cibuildwheel@v3.3.0
60
+ uses: pypa/cibuildwheel@v3.4.1
61
61
  env:
62
62
  CIBW_BUILD: ${{ matrix.cibw_build }}
63
63
  CIBW_SKIP: "pp*"
@@ -119,7 +119,7 @@ jobs:
119
119
  pattern: wheels-*
120
120
  merge-multiple: true
121
121
 
122
- - uses: pypa/gh-action-pypi-publish@v1.13.0
122
+ - uses: pypa/gh-action-pypi-publish@v1.14.0
123
123
  with:
124
124
  user: __token__
125
125
  password: ${{ secrets.pypi_api_token }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: littlefs-python
3
- Version: 0.17.0
3
+ Version: 0.18.0
4
4
  Summary: A python wrapper for littlefs
5
5
  Home-page: https://github.com/jrast/littlefs-python
6
6
  Author: Jürg Rast
@@ -0,0 +1,3 @@
1
+ pytest>=4.0.0
2
+ tox>=3.14.0
3
+ pywin32; sys_platform == "win32"
@@ -58,7 +58,29 @@ if TYPE_CHECKING:
58
58
  class LittleFS:
59
59
  """Littlefs file system"""
60
60
 
61
- def __init__(self, context: Optional["UserContext"] = None, mount=True, **kwargs) -> None:
61
+ def __init__(
62
+ self,
63
+ context: Optional["UserContext"] = None,
64
+ mount=True,
65
+ filename_encoding: Optional[str] = None,
66
+ **kwargs,
67
+ ) -> None:
68
+ """
69
+ Parameters
70
+ ----------
71
+ filename_encoding : Optional[str]
72
+ Encoding used to encode/decode filenames passed to and returned by
73
+ the filesystem. littlefs stores names as opaque byte strings, so this
74
+ is a free choice. Defaults to :data:`littlefs.lfs.FILENAME_ENCODING`
75
+ (``"utf-8"``). Set this when reading an image whose names were written
76
+ with a different encoding (e.g. ``"latin-1"`` or ``"shift-jis"``).
77
+
78
+ Note that littlefs's ``name_max`` limit is measured in *encoded
79
+ bytes*, not characters. With a multi-byte encoding such as UTF-8, a
80
+ single non-ASCII character consumes 2-4 bytes, so a name can exceed
81
+ ``name_max`` (default 255) well before it looks long.
82
+ """
83
+ self.filename_encoding = filename_encoding or lfs.FILENAME_ENCODING
62
84
  self.cfg = lfs.LFSConfig(context=context, **kwargs)
63
85
  self.fs = lfs.LFSFilesystem()
64
86
 
@@ -204,7 +226,7 @@ class LittleFS:
204
226
  buffering = -1
205
227
 
206
228
  try:
207
- fh = lfs.file_open(self.fs, fname, mode)
229
+ fh = lfs.file_open(self.fs, fname, mode, self.filename_encoding)
208
230
  except LittleFSError as e:
209
231
  # Try to map to standard Python exceptions
210
232
  if e.code == LittleFSError.Error.LFS_ERR_NOENT:
@@ -251,15 +273,15 @@ class LittleFS:
251
273
 
252
274
  def getattr(self, path: str, typ: Union[str, bytes, int]) -> bytes:
253
275
  typ = _typ_to_uint8(typ)
254
- return lfs.getattr(self.fs, path, typ)
276
+ return lfs.getattr(self.fs, path, typ, self.filename_encoding)
255
277
 
256
278
  def setattr(self, path: str, typ: Union[str, bytes, int], data: bytes) -> None:
257
279
  typ = _typ_to_uint8(typ)
258
- lfs.setattr(self.fs, path, typ, data)
280
+ lfs.setattr(self.fs, path, typ, data, self.filename_encoding)
259
281
 
260
282
  def removeattr(self, path: str, typ: Union[str, bytes, int]) -> None:
261
283
  typ = _typ_to_uint8(typ)
262
- lfs.removeattr(self.fs, path, typ)
284
+ lfs.removeattr(self.fs, path, typ, self.filename_encoding)
263
285
 
264
286
  def listdir(self, path=".") -> List[str]:
265
287
  """List directory content
@@ -274,7 +296,7 @@ class LittleFS:
274
296
  def mkdir(self, path: str) -> int:
275
297
  """Create a new directory"""
276
298
  try:
277
- return lfs.mkdir(self.fs, path)
299
+ return lfs.mkdir(self.fs, path, self.filename_encoding)
278
300
  except errors.LittleFSError as e:
279
301
  if e.code == LittleFSError.Error.LFS_ERR_EXIST:
280
302
  msg = "[LittleFSError {:d}] Cannot create a file when that file already exists: '{:s}'.".format(
@@ -310,7 +332,7 @@ class LittleFS:
310
332
  If ``true`` and ``path`` is a directory, recursively remove all children files/folders.
311
333
  """
312
334
  try:
313
- lfs.remove(self.fs, path)
335
+ lfs.remove(self.fs, path, self.filename_encoding)
314
336
  return
315
337
  except errors.LittleFSError as e:
316
338
  if e.code == LittleFSError.Error.LFS_ERR_NOENT:
@@ -326,7 +348,7 @@ class LittleFS:
326
348
  # Recursively delete the ``path`` directory
327
349
  for elem in self.scandir(path):
328
350
  self.remove(path + "/" + elem.name, recursive=True)
329
- lfs.remove(self.fs, path)
351
+ lfs.remove(self.fs, path, self.filename_encoding)
330
352
 
331
353
  def removedirs(self, name):
332
354
  """Remove directories recursively
@@ -351,7 +373,7 @@ class LittleFS:
351
373
 
352
374
  def rename(self, src: str, dst: str) -> int:
353
375
  """Rename a file or directory"""
354
- return lfs.rename(self.fs, src, dst)
376
+ return lfs.rename(self.fs, src, dst, self.filename_encoding)
355
377
 
356
378
  def rmdir(self, path: str) -> int:
357
379
  """Remove a directory
@@ -362,17 +384,19 @@ class LittleFS:
362
384
 
363
385
  def scandir(self, path=".") -> Iterator["LFSStat"]:
364
386
  """List directory content"""
365
- dh = lfs.dir_open(self.fs, path)
366
- info = lfs.dir_read(self.fs, dh)
367
- while info:
368
- if info.name not in [".", ".."]:
369
- yield info
370
- info = lfs.dir_read(self.fs, dh)
371
- lfs.dir_close(self.fs, dh)
387
+ dh = lfs.dir_open(self.fs, path, self.filename_encoding)
388
+ try:
389
+ info = lfs.dir_read(self.fs, dh, self.filename_encoding)
390
+ while info:
391
+ if info.name not in [".", ".."]:
392
+ yield info
393
+ info = lfs.dir_read(self.fs, dh, self.filename_encoding)
394
+ finally:
395
+ lfs.dir_close(self.fs, dh)
372
396
 
373
397
  def stat(self, path: str) -> "LFSStat":
374
398
  """Get the status of a file or directory"""
375
- return lfs.stat(self.fs, path)
399
+ return lfs.stat(self.fs, path, self.filename_encoding)
376
400
 
377
401
  def unlink(self, path: str) -> int:
378
402
  """Remove a file or directory
@@ -1,5 +1,6 @@
1
1
  import argparse
2
2
  from contextlib import suppress
3
+ import os
3
4
  from pathlib import Path
4
5
  import sys
5
6
  import textwrap
@@ -7,7 +8,7 @@ import textwrap
7
8
  from littlefs import LittleFS, __version__
8
9
  from littlefs.errors import LittleFSError
9
10
  from littlefs.repl import LittleFSRepl
10
- from littlefs.context import UserContextFile
11
+ from littlefs.context import UserContextFile, UserContext
11
12
 
12
13
  # Dictionary mapping suffixes to their size in bytes
13
14
  _suffix_map = {
@@ -17,13 +18,21 @@ _suffix_map = {
17
18
  }
18
19
 
19
20
 
20
- def _fs_from_args(args: argparse.Namespace, mount=True) -> LittleFS:
21
- return LittleFS(
22
- block_size=args.block_size,
23
- block_count=getattr(args, "block_count", 0),
24
- name_max=args.name_max,
25
- mount=mount,
26
- )
21
+ def _fs_from_args(args: argparse.Namespace, block_count=None, mount=True, context: UserContext = None) -> LittleFS:
22
+ """Build LittleFS from CLI args. Options name_max, attr_max, file_max are stored in the
23
+ superblock and must match when mounting an existing image. inline_max is format-relevant
24
+ (limiting it may improve flash usage)."""
25
+ block_count = block_count if block_count is not None else getattr(args, "block_count", 0)
26
+ kwargs = {
27
+ "block_size": args.block_size,
28
+ "block_count": block_count,
29
+ "name_max": args.name_max,
30
+ "inline_max": args.inline_max,
31
+ "attr_max": args.attr_max,
32
+ "file_max": args.file_max,
33
+ "filename_encoding": getattr(args, "filename_encoding", None),
34
+ }
35
+ return LittleFS(context=context, mount=mount, **kwargs)
27
36
 
28
37
 
29
38
  def size_parser(size_str):
@@ -51,6 +60,16 @@ def size_parser(size_str):
51
60
  return int(size_str, base)
52
61
 
53
62
 
63
+ def _walk_all(root):
64
+ """Recursively yield all paths under root, following symlinks."""
65
+ for dirpath, dirnames, filenames in os.walk(root, followlinks=True):
66
+ dirpath = Path(dirpath)
67
+ for dirname in dirnames:
68
+ yield dirpath / dirname
69
+ for filename in filenames:
70
+ yield dirpath / filename
71
+
72
+
54
73
  def create(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
55
74
  """Create LittleFS image from file/directory contents."""
56
75
  # fs_size OR block_count may be populated; make them consistent.
@@ -68,11 +87,17 @@ def create(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
68
87
  print(f" Image Size: {args.fs_size:9d} / 0x{args.fs_size:X}")
69
88
  print(f" Block Count: {args.block_count:9d}")
70
89
  print(f" Name Max: {args.name_max:9d}")
90
+ if args.inline_max:
91
+ print(f" Inline Max: {args.inline_max:9d} / 0x{args.inline_max:X}")
92
+ if args.attr_max:
93
+ print(f" Attr Max: {args.attr_max:9d}")
94
+ if args.file_max:
95
+ print(f" File Max: {args.file_max:9d}")
71
96
  print(f" Image: {args.destination}")
72
97
 
73
98
  source = Path(args.source).absolute()
74
99
  if source.is_dir():
75
- sources = source.rglob("*")
100
+ sources = list(_walk_all(source))
76
101
  root = source
77
102
  else:
78
103
  sources = [source]
@@ -94,21 +119,14 @@ def create(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
94
119
  if args.compact:
95
120
  if args.verbose:
96
121
  print(f"Compacting... {fs.used_block_count} / {args.block_count}")
97
- compact_fs = LittleFS(
98
- block_size=args.block_size,
99
- block_count=fs.used_block_count,
100
- name_max=args.name_max,
101
- )
102
- for root, dirs, files in fs.walk("/"):
103
- if not root.endswith("/"):
104
- root += "/"
105
- for _dir in dirs:
106
- compact_fs.makedirs(root + _dir, exist_ok=True)
107
- for file in files:
108
- path = root + file
109
- print(path)
110
- with fs.open(path, "rb") as src, compact_fs.open(path, "wb") as dest:
111
- dest.write(src.read())
122
+ compact_fs = _fs_from_args(args, block_count=fs.used_block_count)
123
+ for path in sources:
124
+ rel_path = path.relative_to(root)
125
+ if path.is_dir():
126
+ compact_fs.mkdir(rel_path.as_posix())
127
+ else:
128
+ with compact_fs.open(rel_path.as_posix(), "wb") as dest:
129
+ dest.write(path.read_bytes())
112
130
  compact_fs.fs_grow(args.block_count)
113
131
  data = compact_fs.context.buffer
114
132
  if not args.no_pad:
@@ -121,21 +139,43 @@ def create(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
121
139
  return 0
122
140
 
123
141
 
124
- def _list(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
125
- """List LittleFS image contents."""
126
- fs = _fs_from_args(args, mount=False)
127
- fs.context.buffer = bytearray(args.source.read_bytes())
142
+ def _mount_from_context(parser: argparse.ArgumentParser, args: argparse.Namespace, context: UserContext) -> LittleFS:
143
+ # Block count is 0 because we don't know the size of the real image yet, the source file may be compacted (with the create --compact option).
144
+ fs = _fs_from_args(args, block_count=0, mount=False, context=context)
128
145
  fs.mount()
129
146
 
130
147
  if args.verbose:
131
- fs_size = len(fs.context.buffer)
148
+ input_image_size = context.in_size
149
+ actual_image_size = fs.block_count * args.block_size
132
150
  print("LittleFS Configuration:")
133
151
  print(f" Block Size: {args.block_size:9d} / 0x{args.block_size:X}")
134
- print(f" Image Size: {fs_size:9d} / 0x{fs_size:X}")
152
+ if input_image_size != actual_image_size:
153
+ print(f" Image Size: {actual_image_size:9d} / 0x{actual_image_size:X}")
154
+ print(f" Input Image Size (compacted): {input_image_size:9d} / 0x{input_image_size:X}")
155
+ else:
156
+ print(f" Image Size: {input_image_size:9d} / 0x{input_image_size:X}")
135
157
  print(f" Block Count: {fs.block_count:9d}")
136
158
  print(f" Name Max: {args.name_max:9d}")
159
+ if args.inline_max:
160
+ print(f" Inline Max: {args.inline_max:9d} / 0x{args.inline_max:X}")
161
+ if args.attr_max:
162
+ print(f" Attr Max: {args.attr_max:9d}")
163
+ if args.file_max:
164
+ print(f" File Max: {args.file_max:9d}")
137
165
  print(f" Image: {args.source}")
138
166
 
167
+ return fs
168
+
169
+
170
+ def _list(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
171
+ """List LittleFS image contents."""
172
+ source: Path = args.source
173
+ if not source.is_file():
174
+ parser.error(f"Source image '{source}' does not exist.")
175
+ context = UserContext(buffer=bytearray(source.read_bytes()))
176
+
177
+ fs = _mount_from_context(parser, args, context)
178
+
139
179
  for root, dirs, files in fs.walk("/"):
140
180
  if not root.endswith("/"):
141
181
  root += "/"
@@ -148,18 +188,12 @@ def _list(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
148
188
 
149
189
  def extract(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
150
190
  """Extract LittleFS image contents to a directory."""
151
- fs = _fs_from_args(args, mount=False)
152
- fs.context.buffer = bytearray(args.source.read_bytes())
153
- fs.mount()
191
+ source: Path = args.source
192
+ if not source.is_file():
193
+ parser.error(f"Source image '{source}' does not exist.")
194
+ context = UserContext(buffer=bytearray(source.read_bytes()))
154
195
 
155
- if args.verbose:
156
- fs_size = len(fs.context.buffer)
157
- print("LittleFS Configuration:")
158
- print(f" Block Size: {args.block_size:9d} / 0x{args.block_size:X}")
159
- print(f" Image Size: {fs_size:9d} / 0x{fs_size:X}")
160
- print(f" Block Count: {fs.block_count:9d}")
161
- print(f" Name Max: {args.name_max:9d}")
162
- print(f" Image: {args.source}")
196
+ fs = _mount_from_context(parser, args, context)
163
197
 
164
198
  root_dest = args.destination.absolute()
165
199
  if not root_dest.exists():
@@ -192,36 +226,19 @@ def extract(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
192
226
 
193
227
  def repl(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
194
228
  """Inspect an existing LittleFS image through an interactive shell."""
195
-
196
229
  source: Path = args.source
197
230
  if not source.is_file():
198
231
  parser.error(f"Source image '{source}' does not exist.")
232
+ context = UserContextFile(str(source)) # In repl we want context to be the file itself, so commands will change it
199
233
 
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
234
  try:
221
235
  try:
222
- shell.do_mount()
236
+ fs = _mount_from_context(parser, args, context)
223
237
  except LittleFSError as exc:
224
238
  parser.error(f"Failed to mount '{source}': {exc}")
239
+
240
+ shell = LittleFSRepl(fs)
241
+
225
242
  shell.cmdloop()
226
243
  finally:
227
244
  if shell._mounted:
@@ -252,12 +269,41 @@ def get_parser():
252
269
 
253
270
  common_parser = argparse.ArgumentParser(add_help=False)
254
271
  common_parser.add_argument("-v", "--verbose", action="count", default=0)
272
+ # Stored in superblock; must match when mounting an existing image:
255
273
  common_parser.add_argument(
256
274
  "--name-max",
257
275
  type=size_parser,
258
276
  default=255,
259
277
  help="LittleFS max file path length. Defaults to LittleFS's default (255).",
260
278
  )
279
+ common_parser.add_argument(
280
+ "--attr-max",
281
+ type=int,
282
+ default=0,
283
+ help="Max custom attribute size per file. Defaults to LittleFS's default (0 = use library default).",
284
+ )
285
+ common_parser.add_argument(
286
+ "--file-max",
287
+ type=int,
288
+ default=0,
289
+ help="Max number of open files. Defaults to LittleFS's default (0 = use library default).",
290
+ )
291
+ # Format option: limiting inline_max may improve flash usage.
292
+ common_parser.add_argument(
293
+ "--inline-max",
294
+ type=size_parser,
295
+ default=0,
296
+ help="Max inline file size; 0 = use library default. Limiting can improve flash usage.",
297
+ )
298
+ # Host-side encode/decode choice; never stored in the image. The same encoding
299
+ # must be used to extract an image as was used to create it, otherwise filenames
300
+ # will fail to decode or come out as mojibake.
301
+ common_parser.add_argument(
302
+ "--filename-encoding",
303
+ default=None,
304
+ help="Encoding for filenames stored in the image. Defaults to utf-8. "
305
+ "Use e.g. latin-1 or shift-jis for images whose names use a different encoding.",
306
+ )
261
307
 
262
308
  subparsers = parser.add_subparsers(required=True, title="Available Commands", dest="command")
263
309
 
@@ -358,10 +404,13 @@ def get_parser():
358
404
  return parser
359
405
 
360
406
 
361
- def main():
407
+ # Getting argv optionally from the caller to enable call from python (generally for testing, but could be used for other purposes)
408
+ def main(argv=None):
409
+ if argv is None:
410
+ argv = sys.argv
362
411
  parser = get_parser()
363
- parser.parse_known_args(sys.argv[1:]) # Allows for ``littlefs-python --version``
364
- args = parser.parse_args(sys.argv[1:])
412
+ parser.parse_known_args(argv[1:]) # Allows for ``littlefs-python --version``
413
+ args = parser.parse_args(argv[1:])
365
414
  return args.func(parser, args)
366
415
 
367
416
 
@@ -10,8 +10,14 @@ if typing.TYPE_CHECKING:
10
10
  class UserContext:
11
11
  """Basic User Context Implementation"""
12
12
 
13
- def __init__(self, buffsize: int) -> None:
14
- self.buffer = bytearray([0xFF] * buffsize)
13
+ def __init__(self, buffsize: int = None, buffer: bytearray = None) -> None:
14
+ if buffer is not None:
15
+ self.buffer = buffer
16
+ elif buffsize is not None:
17
+ self.buffer = bytearray([0xFF] * buffsize)
18
+ else:
19
+ raise ValueError("Either buffsize or buffer must be provided")
20
+ self.in_size = len(self.buffer)
15
21
 
16
22
  def read(self, cfg: "LFSConfig", block: int, off: int, size: int) -> bytearray:
17
23
  """read data
@@ -91,6 +97,7 @@ class UserContextFile(UserContext):
91
97
 
92
98
  self._path = file_path
93
99
  self._fh = open(file_path, mode)
100
+ self.in_size = os.path.getsize(file_path)
94
101
 
95
102
  def read(self, cfg: "LFSConfig", block: int, off: int, size: int) -> bytearray:
96
103
  logging.getLogger(__name__).debug("LFS Read : Block: %d, Offset: %d, Size=%d" % (block, off, size))
@@ -150,10 +157,11 @@ class UserContextWinDisk(UserContext):
150
157
  "Unable to import 'win32file'. This module is required for Windows-specific functionality. Please ensure you are running on a Windows platform or install 'pywin32' using: 'pip install pywin32'."
151
158
  )
152
159
  self.device = win32file.CreateFile(
153
- disk_path, win32file.GENERIC_READ, win32file.FILE_SHARE_READ, None, win32file.OPEN_EXISTING, 0, None
160
+ disk_path, win32file.GENERIC_READ | win32file.GENERIC_WRITE, win32file.FILE_SHARE_READ, None, win32file.OPEN_EXISTING, 0, None
154
161
  )
155
162
  if self.device == win32file.INVALID_HANDLE_VALUE:
156
163
  raise IOError("Could not open disk %s" % disk_path)
164
+ self.in_size = win32file.GetFileSize(self.device)
157
165
 
158
166
  def read(self, cfg: "LFSConfig", block: int, off: int, size: int) -> bytearray:
159
167
  """read data
@@ -214,7 +222,7 @@ class UserContextWinDisk(UserContext):
214
222
  start = block * cfg.block_size
215
223
 
216
224
  win32file.SetFilePointer(self.device, start, win32file.FILE_BEGIN)
217
- win32file.WriteFile(self.device, [0xFF] * cfg.block_size)
225
+ win32file.WriteFile(self.device, b'\xff' * cfg.block_size)
218
226
  return 0
219
227
 
220
228
  def sync(self, cfg: "LFSConfig") -> int: