1import binascii
2import io
3from typing import Optional
4
5from structlog import get_logger
6
7from unblob.file_utils import (
8 Endian,
9 InvalidInputFormat,
10 convert_int16,
11 read_until_past,
12 round_up,
13)
14
15from ...extractors import Command
16from ...models import (
17 File,
18 HandlerDoc,
19 HandlerType,
20 HexString,
21 Reference,
22 StructHandler,
23 ValidChunk,
24)
25
26logger = get_logger()
27
28
29BLOCK_ALIGNMENT = 4
30JFFS2_MAGICS = [0x1985, 0x8519, 0x1984, 0x8419]
31
32# Compatibility flags.
33JFFS2_NODE_ACCURATE = 0x2000
34JFFS2_FEATURE_INCOMPAT = 0xC000
35JFFS2_FEATURE_RWCOMPAT_DELETE = 0x0000
36
37DIRENT = JFFS2_FEATURE_INCOMPAT | JFFS2_NODE_ACCURATE | 1
38INODE = JFFS2_FEATURE_INCOMPAT | JFFS2_NODE_ACCURATE | 2
39CLEANMARKER = JFFS2_FEATURE_RWCOMPAT_DELETE | JFFS2_NODE_ACCURATE | 3
40PADDING = JFFS2_FEATURE_RWCOMPAT_DELETE | JFFS2_NODE_ACCURATE | 4
41SUMMARY = JFFS2_FEATURE_RWCOMPAT_DELETE | JFFS2_NODE_ACCURATE | 6
42XATTR = JFFS2_FEATURE_INCOMPAT | JFFS2_NODE_ACCURATE | 8
43XREF = JFFS2_FEATURE_INCOMPAT | JFFS2_NODE_ACCURATE | 9
44
45JFFS2_NODETYPES = {DIRENT, INODE, CLEANMARKER, PADDING, SUMMARY, XATTR, XREF}
46
47
48class _JFFS2Base(StructHandler):
49 C_DEFINITIONS = r"""
50 typedef struct jffs2_unknown_node
51 {
52 uint16 magic;
53 uint16 nodetype;
54 uint32 totlen;
55 uint32 hdr_crc;
56 } jffs2_unknown_node_t;
57 """
58
59 HEADER_STRUCT = "jffs2_unknown_node_t"
60
61 BIG_ENDIAN_MAGIC = 0x19_85
62
63 EXTRACTOR = Command("jefferson", "-v", "-f", "-d", "{outdir}", "{inpath}")
64
65 def guess_endian(self, file: File) -> Endian:
66 magic = convert_int16(file.read(2), Endian.BIG)
67 endian = Endian.BIG if magic == self.BIG_ENDIAN_MAGIC else Endian.LITTLE
68 file.seek(-2, io.SEEK_CUR)
69 return endian
70
71 def valid_header(self, header, node_start_offset: int, eof: int) -> bool:
72 header_crc = (binascii.crc32(header.dumps()[:-4], -1) ^ -1) & 0xFFFFFFFF
73 check_crc = True
74
75 if header.nodetype not in JFFS2_NODETYPES:
76 if header.nodetype | JFFS2_NODE_ACCURATE not in JFFS2_NODETYPES:
77 logger.debug(
78 "Invalid JFFS2 node type", node_type=header.nodetype, _verbosity=2
79 )
80 return False
81 logger.debug(
82 "Not accurate JFFS2 node type, ignore CRC",
83 node_type=header.nodetype,
84 _verbosity=2,
85 )
86 check_crc = False
87
88 if check_crc and header_crc != header.hdr_crc:
89 logger.debug("node header CRC missmatch", _verbosity=2)
90 return False
91
92 if node_start_offset + header.totlen > eof:
93 logger.debug(
94 "node length greater than total file size",
95 node_len=header.totlen,
96 file_size=eof,
97 _verbosity=2,
98 )
99 return False
100
101 if header.totlen < len(header):
102 logger.debug(
103 "node length greater than header size",
104 node_len=header.totlen,
105 _verbosity=2,
106 )
107 return False
108 return True
109
110 def calculate_chunk(self, file: File, start_offset: int) -> Optional[ValidChunk]:
111 file.seek(0, io.SEEK_END)
112 eof = file.tell()
113 file.seek(start_offset)
114
115 endian = self.guess_endian(file)
116 current_offset = start_offset
117
118 while current_offset < eof:
119 node_start_offset = current_offset
120 file.seek(current_offset)
121 try:
122 header = self.parse_header(file, endian=endian)
123 except EOFError:
124 break
125
126 if header.magic not in JFFS2_MAGICS:
127 # JFFS2 allows padding at the end with 0xFF or 0x00, usually
128 # to the size of an erase block.
129 if header.magic in [0x0000, 0xFFFF]:
130 file.seek(-len(header), io.SEEK_CUR)
131 current_offset = read_until_past(file, b"\x00\xff")
132 continue
133
134 logger.debug(
135 "unexpected header magic",
136 header_magic=header.magic,
137 _verbosity=2,
138 )
139 break
140
141 if not self.valid_header(header, node_start_offset, eof):
142 return None
143
144 node_len = round_up(header.totlen, BLOCK_ALIGNMENT)
145 current_offset += node_len
146
147 if current_offset > eof:
148 raise InvalidInputFormat("Corrupt file or last chunk isn't really JFFS2")
149
150 return ValidChunk(
151 start_offset=start_offset,
152 end_offset=current_offset,
153 )
154
155
156class JFFS2OldHandler(_JFFS2Base):
157 NAME = "jffs2_old"
158
159 PATTERNS = [
160 HexString("84 19 ( 01 | 02 | 03 | 04 | 06 | 08 | 09 ) ( e0 | 20 )"), # LE
161 HexString("19 84 ( e0 | 20 ) ( 01 | 02 | 03 | 04 | 06 | 08 | 09 )"), # BE
162 ]
163
164 BIG_ENDIAN_MAGIC = 0x19_84
165
166 DOC = HandlerDoc(
167 name="JFFS2 (old)",
168 description="JFFS2 (Journaling Flash File System version 2) is a log-structured file system for flash memory devices, using an older magic number to identify its nodes. It organizes data into nodes with headers containing metadata and CRC checks for integrity.",
169 handler_type=HandlerType.FILESYSTEM,
170 vendor=None,
171 references=[
172 Reference(
173 title="JFFS2 Documentation",
174 url="https://sourceware.org/jffs2/",
175 ),
176 Reference(
177 title="JFFS2 Wikipedia",
178 url="https://en.wikipedia.org/wiki/JFFS2",
179 ),
180 ],
181 limitations=[],
182 )
183
184
185class JFFS2NewHandler(_JFFS2Base):
186 NAME = "jffs2_new"
187
188 PATTERNS = [
189 HexString("85 19 ( 01 | 02 | 03 | 04 | 06 | 08 | 09 ) ( e0 | 20 )"), # LE
190 HexString("19 85 ( e0 | 20 ) ( 01 | 02 | 03 | 04 | 06 | 08 | 09 )"), # BE
191 ]
192
193 BIG_ENDIAN_MAGIC = 0x19_85
194
195 DOC = HandlerDoc(
196 name="JFFS2 (new)",
197 description="JFFS2 (Journaling Flash File System version 2) is a log-structured file system for flash memory devices, using an older magic number to identify its nodes. It organizes data into nodes with headers containing metadata and CRC checks for integrity.",
198 handler_type=HandlerType.FILESYSTEM,
199 vendor=None,
200 references=[
201 Reference(
202 title="JFFS2 Documentation",
203 url="https://sourceware.org/jffs2/",
204 ),
205 Reference(
206 title="JFFS2 Wikipedia",
207 url="https://en.wikipedia.org/wiki/JFFS2",
208 ),
209 ],
210 limitations=[],
211 )