Skip to content

Commit 4e72ba9

Browse files
committed
improved ERFS tool
1 parent 851625c commit 4e72ba9

File tree

1 file changed

+138
-11
lines changed

1 file changed

+138
-11
lines changed

mkerfs32.py

+138-11
Original file line numberDiff line numberDiff line change
@@ -45,27 +45,50 @@
4545
#
4646

4747
import click
48+
from collections import namedtuple
49+
from datetime import datetime
50+
from fnmatch import fnmatch
51+
import os
4852
from pathlib import Path
4953
import struct
54+
import sys
55+
import time
56+
5057

5158
HEADER_SIZE = 8
5259
FATRECORD_SIZE = 16
5360

61+
FATRecord = namedtuple("FATRecord", ["StringPtr", "DataPtr", "Len", "Timestamp"])
62+
63+
64+
def get_string(fs: bytes, ptr: int, hex_name=False) -> str:
65+
if fs[ptr] == 0:
66+
if hex_name:
67+
return f"{ptr:06X}"
68+
else:
69+
return "<no name>"
70+
s = ""
71+
for i in fs[ptr:]:
72+
if i == 0:
73+
break
74+
elif i < 32:
75+
s += f"<{i}>"
76+
else:
77+
s += chr(i)
78+
return s
5479

55-
def align32(i):
80+
81+
def align32(i: int) -> int:
5682
if i & 3 == 0:
5783
return i
5884
else:
5985
return (i | 3) + 1
6086

6187

62-
@click.command()
63-
@click.option("-s", "--size", help="fs image size, in bytes", type=str, default="1m", show_default=True)
64-
@click.option("-c", "--create", help="create image from a directory", type=str, default="data", show_default=True)
65-
@click.option("-b", "--block", help="ignored", type=int, expose_value=False)
66-
@click.option("-p", "--page", help="ignored", type=int, expose_value=False)
67-
@click.argument("image_file")
68-
def main(size, create, image_file):
88+
def create(data_dir: str, size: str, image_file: str) -> None:
89+
"""
90+
Create a ERFS filesystem image.
91+
"""
6992

7093
if size[-1].lower() == "k":
7194
size = int(size[:-1]) * 1024
@@ -78,14 +101,14 @@ def main(size, create, image_file):
78101

79102
total_size = HEADER_SIZE # the header
80103
files = []
81-
for f in Path(create).rglob("*"):
104+
for f in Path(data_dir).rglob("*"):
82105
if f.is_dir():
83106
continue
84107

85108
if f.name in [".DS_Store", ".git"]:
86109
continue
87110

88-
name = f.relative_to(create).as_posix().encode()
111+
name = f.relative_to(data_dir).as_posix().encode()
89112

90113
total_size += 2 + FATRECORD_SIZE + align32(len(name) + 1) + align32(f.stat().st_size)
91114
files.append((f, name, f.stat()))
@@ -95,7 +118,7 @@ def main(size, create, image_file):
95118
total_size = align32(total_size) # need to align because of the hash table
96119

97120
if total_size > size:
98-
print(f"FS too small, missing {total_size - size} bytes")
121+
print(f"FS too small, missing {total_size - size} bytes", file=sys.stderr)
99122
exit(2)
100123

101124
num_files = len(files)
@@ -157,5 +180,109 @@ def main(size, create, image_file):
157180
print(f"{image_file} written")
158181

159182

183+
def list_content(image_file: str, verbose: bool, extract_dir: str, patterns) -> None:
184+
"""
185+
List or extract the content of a ERFS image.
186+
"""
187+
188+
fs = Path(image_file).read_bytes()
189+
190+
# process filesystem header
191+
signature, ver_hi, ver_lo, n = struct.unpack("<4sBBH", fs[0:HEADER_SIZE])
192+
if signature != b"ERFS":
193+
print("File is not a ERFS filesystem", file=sys.stderr)
194+
exit(2)
195+
196+
offset = HEADER_SIZE
197+
name_hash = [0] * n
198+
for i in range(n):
199+
(name_hash[i],) = struct.unpack("<H", fs[offset : offset + 2])
200+
offset += 2
201+
offset = align32(offset)
202+
203+
record = [None] * n
204+
for i in range(n):
205+
record[i] = FATRecord._make(struct.unpack("<IIII", fs[offset : offset + FATRECORD_SIZE]))
206+
offset += FATRECORD_SIZE
207+
208+
if extract_dir:
209+
if extract_dir != "-":
210+
extract_dir = Path(extract_dir)
211+
else:
212+
print(f"Version: {ver_hi}.{ver_lo}")
213+
print(f"Number of files: {n}")
214+
215+
for i, r in enumerate(record, 1):
216+
217+
if fs[r.StringPtr] == 0:
218+
print(f"Bad file entry {i}: should have a name", i, file=sys.stderr)
219+
break
220+
221+
filename = get_string(fs, r.StringPtr)
222+
223+
if extract_dir:
224+
225+
if patterns:
226+
if not any(fnmatch(filename, pattern) for pattern in patterns):
227+
continue
228+
229+
if extract_dir == "-":
230+
print(f"extracted {i}/{n}: {filename} {r.Len} bytes", file=sys.stderr)
231+
sys.stdout.buffer.write(fs[r.DataPtr : r.DataPtr + r.Len])
232+
else:
233+
f = extract_dir / filename
234+
f.parent.mkdir(exist_ok=True, parents=True)
235+
236+
f.write_bytes(fs[r.DataPtr : r.DataPtr + r.Len])
237+
238+
timestamp = datetime.fromtimestamp(r.Timestamp)
239+
mt = time.mktime(timestamp.timetuple())
240+
os.utime(f.as_posix(), (mt, mt))
241+
242+
print(f"extracted {i}/{n}: {f.as_posix()} {r.Len} bytes")
243+
244+
else:
245+
timestamp = datetime.fromtimestamp(r.Timestamp).strftime("%Y-%m-%dT%H:%M:%SZ")
246+
247+
if verbose:
248+
print()
249+
print(f"FATRecord {i}:")
250+
print(f" .StringPtr = 0x{r.StringPtr:06x} {filename}")
251+
print(f" .DataPtr = 0x{r.DataPtr:06x}")
252+
print(f" .Len = 0x{r.Len:06x} {r.Len}")
253+
print(f" .Timestamp =", r.Timestamp, timestamp)
254+
255+
else:
256+
print(f"{i:4d} {r.Len:8d} {timestamp} {filename}")
257+
258+
259+
@click.command(context_settings={"help_option_names": ["-h", "--help"]})
260+
@click.option(
261+
"-c",
262+
"--create",
263+
"data_dir",
264+
metavar="DATA_DIR",
265+
help="create image from a directory",
266+
type=str,
267+
default="data",
268+
show_default=True,
269+
)
270+
@click.option("-l", "--list", "list_files", help="list the content of an image", is_flag=True)
271+
@click.option("-x", "--extract", "extract_dir", metavar="DIR", help="extract files to directory", type=str)
272+
@click.option("-s", "--size", metavar="SIZE", help="fs image size, in bytes", type=str, default="1m", show_default=True)
273+
@click.option("-b", "--block", help="ignored", type=int, expose_value=False)
274+
@click.option("-p", "--page", help="ignored", type=int, expose_value=False)
275+
@click.option("-v", "--verbose", help="verbose list", is_flag=True)
276+
@click.argument("image_file")
277+
@click.argument("files", metavar="[FILES_TO_EXTRACT]", nargs=-1)
278+
def main(data_dir, list_files, extract_dir, size, verbose, image_file, files):
279+
280+
if list_files or extract_dir:
281+
list_content(image_file, verbose, extract_dir, files)
282+
283+
else:
284+
create(data_dir, size, image_file)
285+
286+
160287
if __name__ == "__main__":
161288
main()

0 commit comments

Comments
 (0)