1
1
# SPDX-License-Identifier: Apache-2.0
2
2
"""Analyze archive members"""
3
3
4
+ import logging
5
+ import os
4
6
import pathlib
5
7
import stat
6
8
import tarfile
7
9
import typing
8
10
import zipfile
9
11
12
+ from elftools .common .exceptions import ELFError
10
13
from elftools .elf .elffile import ELFFile
11
14
12
15
from ._elfdeps import ELFAnalyzeSettings , ELFInfo , analyze_elffile
16
+ from ._fileinfo import is_executable_file
17
+
18
+ logger = logging .getLogger (__name__ )
19
+
20
+
21
+ def _zipinfo_mode (zipinfo : zipfile .ZipInfo ) -> int :
22
+ """Full mode for zipinfo object"""
23
+ # mode may not contain reg file info
24
+ mode = zipinfo .external_attr >> 16
25
+ if stat .S_IFMT (mode ) == 0 :
26
+ lo = zipinfo .external_attr & 0xFFFF
27
+ if lo & 0x10 :
28
+ # MS-DOS directory
29
+ mode |= stat .S_IFDIR
30
+ else :
31
+ mode |= stat .S_IFREG
32
+ return mode
13
33
14
34
15
35
def analyze_zipmember (
@@ -19,8 +39,8 @@ def analyze_zipmember(
19
39
settings : ELFAnalyzeSettings | None = None ,
20
40
) -> ELFInfo :
21
41
"""Analyze a zipfile member"""
22
- mode = zipinfo . external_attr >> 16
23
- is_exec = bool (mode & ( stat . S_IXUSR | stat . S_IXGRP | stat . S_IXOTH ) )
42
+ mode = _zipinfo_mode ( zipinfo )
43
+ is_exec = is_executable_file (mode )
24
44
filename = pathlib .Path (zipinfo .filename )
25
45
with zfile .open (zipinfo , mode = "r" ) as f :
26
46
elffile = ELFFile (f )
@@ -29,15 +49,51 @@ def analyze_zipmember(
29
49
)
30
50
31
51
52
+ def analyze_zipfile (
53
+ zfile : zipfile .ZipFile , * , settings : ELFAnalyzeSettings | None = None
54
+ ) -> typing .Generator [ELFInfo , None , None ]:
55
+ """Analyze a zip file"""
56
+ if settings is None :
57
+ settings = ELFAnalyzeSettings ()
58
+ for zipinfo in zfile .infolist ():
59
+ filename = pathlib .Path (zipinfo .filename )
60
+ mode = _zipinfo_mode (zipinfo )
61
+ if settings .is_candidate (filename , mode ):
62
+ try :
63
+ yield analyze_zipmember (zfile , zipinfo , settings = settings )
64
+ except ELFError as err :
65
+ # not an ELF file (e.g. a script or linker script)
66
+ logger .debug ("%s is not a ELF file: %s" , filename , err )
67
+
68
+
69
+ def _tarinfo_mode (tarinfo : tarfile .TarInfo ) -> int :
70
+ """Full mode for tarinfo"""
71
+ # tarinfo.mode contains only permission bits
72
+ mode = tarinfo .mode
73
+ if tarinfo .isreg ():
74
+ mode |= stat .S_IFREG
75
+ elif tarinfo .isdir ():
76
+ mode |= stat .S_IFDIR
77
+ elif tarinfo .issym ():
78
+ mode |= stat .S_IFLNK
79
+ elif tarinfo .isblk ():
80
+ mode |= stat .S_IFBLK
81
+ elif tarinfo .ischr ():
82
+ mode |= stat .S_IFCHR
83
+ elif tarinfo .isfifo ():
84
+ mode |= stat .S_IFIFO
85
+ return mode
86
+
87
+
32
88
def analyze_tarmember (
33
89
tfile : tarfile .TarFile ,
34
90
tarinfo : tarfile .TarInfo ,
35
91
* ,
36
92
settings : ELFAnalyzeSettings | None = None ,
37
93
) -> ELFInfo :
38
94
"""Analze a tarfile member"""
39
- mode = tarinfo . mode
40
- is_exec = bool (mode & ( stat . S_IXUSR | stat . S_IXGRP | stat . S_IXOTH ) )
95
+ mode = _tarinfo_mode ( tarinfo )
96
+ is_exec = is_executable_file (mode )
41
97
filename = pathlib .Path (tarinfo .name )
42
98
f = tfile .extractfile (tarinfo )
43
99
if typing .TYPE_CHECKING :
@@ -47,3 +103,82 @@ def analyze_tarmember(
47
103
return analyze_elffile (
48
104
elffile , filename = filename , is_exec = is_exec , settings = settings
49
105
)
106
+
107
+
108
+ def analyze_tarfile (
109
+ tfile : tarfile .TarFile , * , settings : ELFAnalyzeSettings | None = None
110
+ ) -> typing .Generator [ELFInfo , None , None ]:
111
+ """Analyze a tar ball"""
112
+ if settings is None :
113
+ settings = ELFAnalyzeSettings ()
114
+ for tarinfo in tfile :
115
+ filename = pathlib .Path (tarinfo .name )
116
+ mode = _tarinfo_mode (tarinfo )
117
+ if settings .is_candidate (filename , mode ):
118
+ try :
119
+ yield analyze_tarmember (tfile , tarinfo , settings = settings )
120
+ except ELFError as err :
121
+ # not an ELF file (e.g. a script or linker script)
122
+ logger .debug ("%s is not a ELF file: %s" , filename , err )
123
+
124
+
125
+ OnError = typing .Callable [[pathlib .Path , OSError | ELFError ], None ] | None
126
+
127
+
128
+ def _scanwalk (
129
+ dirname : pathlib .Path , onerror : OnError = None
130
+ ) -> typing .Generator [os .DirEntry , None , None ]:
131
+ """Recursive scandir"""
132
+ try :
133
+ it = os .scandir (dirname )
134
+ except OSError as err :
135
+ if onerror is not None :
136
+ onerror (dirname , err )
137
+ return
138
+
139
+ with it :
140
+ while True :
141
+ try :
142
+ entry = next (it )
143
+ except StopIteration :
144
+ break
145
+ except OSError as err :
146
+ if onerror is not None :
147
+ onerror (dirname , err )
148
+ return
149
+ try :
150
+ is_dir = entry .is_dir (follow_symlinks = False )
151
+ except OSError :
152
+ is_dir = False
153
+ if is_dir :
154
+ yield from _scanwalk (pathlib .Path (entry .path ), onerror = onerror )
155
+ else :
156
+ yield entry
157
+
158
+
159
+ def analyze_dirtree (
160
+ dirname : pathlib .Path ,
161
+ * ,
162
+ settings : ELFAnalyzeSettings | None = None ,
163
+ onerror : OnError = None ,
164
+ ) -> typing .Generator [ELFInfo , None , None ]:
165
+ """Recursively analyze dirctory tree"""
166
+ if settings is None :
167
+ settings = ELFAnalyzeSettings ()
168
+ for entry in _scanwalk (dirname ):
169
+ filename = pathlib .Path (entry .path )
170
+ try :
171
+ mode = entry .stat (follow_symlinks = False ).st_mode
172
+ if settings .is_candidate (filename , mode ):
173
+ with filename .open ("rb" ) as f :
174
+ elffile = ELFFile (f )
175
+ yield analyze_elffile (
176
+ elffile ,
177
+ filename = filename ,
178
+ is_exec = is_executable_file (mode ),
179
+ settings = settings ,
180
+ )
181
+ except (OSError , ELFError ) as err :
182
+ logger .debug ("%s is not a ELF file or is not accessible: %s" , filename , err )
183
+ if onerror is not None :
184
+ onerror (filename , err )
0 commit comments