45
45
#
46
46
47
47
import click
48
+ from collections import namedtuple
49
+ from datetime import datetime
50
+ from fnmatch import fnmatch
51
+ import os
48
52
from pathlib import Path
49
53
import struct
54
+ import sys
55
+ import time
56
+
50
57
51
58
HEADER_SIZE = 8
52
59
FATRECORD_SIZE = 16
53
60
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
54
79
55
- def align32 (i ):
80
+
81
+ def align32 (i : int ) -> int :
56
82
if i & 3 == 0 :
57
83
return i
58
84
else :
59
85
return (i | 3 ) + 1
60
86
61
87
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
+ """
69
92
70
93
if size [- 1 ].lower () == "k" :
71
94
size = int (size [:- 1 ]) * 1024
@@ -78,14 +101,14 @@ def main(size, create, image_file):
78
101
79
102
total_size = HEADER_SIZE # the header
80
103
files = []
81
- for f in Path (create ).rglob ("*" ):
104
+ for f in Path (data_dir ).rglob ("*" ):
82
105
if f .is_dir ():
83
106
continue
84
107
85
108
if f .name in [".DS_Store" , ".git" ]:
86
109
continue
87
110
88
- name = f .relative_to (create ).as_posix ().encode ()
111
+ name = f .relative_to (data_dir ).as_posix ().encode ()
89
112
90
113
total_size += 2 + FATRECORD_SIZE + align32 (len (name ) + 1 ) + align32 (f .stat ().st_size )
91
114
files .append ((f , name , f .stat ()))
@@ -95,7 +118,7 @@ def main(size, create, image_file):
95
118
total_size = align32 (total_size ) # need to align because of the hash table
96
119
97
120
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 )
99
122
exit (2 )
100
123
101
124
num_files = len (files )
@@ -157,5 +180,109 @@ def main(size, create, image_file):
157
180
print (f"{ image_file } written" )
158
181
159
182
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
+
160
287
if __name__ == "__main__" :
161
288
main ()
0 commit comments