1import io
2
3from unblob.extractors import Command
4from unblob.file_utils import (
5 Endian,
6 InvalidInputFormat,
7 snull,
8)
9from unblob.models import (
10 File,
11 HandlerDoc,
12 HandlerType,
13 HexString,
14 Reference,
15 StructHandler,
16 ValidChunk,
17)
18
19C_DEFINITIONS = r"""
20 typedef struct erofs_handler{
21 uint32_t magic;
22 uint32_t crc32c;
23 uint32_t feature_compact;
24 uint8_t block_size_bs;
25 uint8_t sb_extslots;
26 uint16_t root_nid;
27 uint64_t inos;
28 uint64_t build_time;
29 uint32_t build_time_nsec;
30 uint32_t block_count;
31 uint32_t meta_blkaddr;
32 uint32_t xattr_blkaddr;
33 uint8_t uuid[16];
34 char volume_name[16];
35 uint32_t feature_incompact;
36 char reserved[44];
37 } erofs_handler_t;
38"""
39
40SUPERBLOCK_OFFSET = 0x400
41
42
43class EROFSHandler(StructHandler):
44 NAME = "erofs"
45 PATTERNS = [HexString("e2 e1 f5 e0")] # Magic in little endian
46 HEADER_STRUCT = "erofs_handler_t"
47 C_DEFINITIONS = C_DEFINITIONS
48 EXTRACTOR = Command(
49 "fsck.erofs",
50 "--no-preserve",
51 "--extract={outdir}",
52 "{inpath}",
53 )
54 PATTERN_MATCH_OFFSET = -SUPERBLOCK_OFFSET
55
56 DOC = HandlerDoc(
57 name="Android EROFS",
58 description="EROFS (Enhanced Read-Only File System) is a lightweight, high-performance file system designed for read-only use cases, commonly used in Android and Linux. It features compression support, metadata efficiency, and a fixed superblock structure.",
59 handler_type=HandlerType.FILESYSTEM,
60 vendor="Google",
61 references=[
62 Reference(
63 title="EROFS Documentation",
64 url="https://www.kernel.org/doc/html/latest/filesystems/erofs.html",
65 ),
66 Reference(
67 title="EROFS Wikipedia",
68 url="https://en.wikipedia.org/wiki/Enhanced_Read-Only_File_System",
69 ),
70 ],
71 limitations=[],
72 )
73
74 def is_valid_header(self, header) -> bool:
75 try:
76 snull(header.volume_name).decode("utf-8")
77 except UnicodeDecodeError:
78 return False
79 return (
80 header.block_count >= 1
81 and header.build_time > 0
82 and header.build_time_nsec > 0
83 and header.block_size_bs >= 9
84 )
85
86 def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None:
87 file.seek(start_offset + SUPERBLOCK_OFFSET, io.SEEK_SET)
88 header = self.parse_header(file, Endian.LITTLE)
89 if not self.is_valid_header(header):
90 raise InvalidInputFormat("Invalid erofs header.")
91
92 end_offset = (1 << header.block_size_bs) * header.block_count
93 return ValidChunk(
94 start_offset=start_offset,
95 end_offset=end_offset,
96 )