Skip to content

Commit 63257d9

Browse files
committed
Add functionality to download and compile Rizin
1 parent 53a295e commit 63257d9

File tree

2 files changed

+267
-0
lines changed

2 files changed

+267
-0
lines changed

quark/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,9 @@
99
DIR_PATH = f"{HOME_DIR}quark-rules"
1010

1111
DEBUG = False
12+
COMPATIBLE_RAZIN_VERSIONS = ["0.4.0"]
13+
14+
RIZIN_DIR = f"{HOME_DIR}rizin/"
15+
RIZIN_COMMIT = "de8a5cac5532845643a52d1231b17a7b34feb50a"
1216

1317
Path(HOME_DIR).mkdir(parents=True, exist_ok=True)

quark/utils/tools.py

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@
44

55
import copy
66
import re
7+
import shutil
8+
import subprocess
9+
from ast import Str
10+
from os import F_OK, PathLike, access, mkdir
11+
from xmlrpc.client import Boolean
12+
13+
from quark.config import COMPATIBLE_RAZIN_VERSIONS, RIZIN_COMMIT, RIZIN_DIR
14+
from quark.utils.pprint import (
15+
clear_the_last_line,
16+
print_error,
17+
print_info,
18+
print_success,
19+
)
720

821

922
def remove_dup_list(element):
@@ -55,3 +68,253 @@ def descriptor_to_androguard_format(descriptor):
5568
new_descriptor = re.sub(r"\[ ", "[", new_descriptor)
5669

5770
return new_descriptor
71+
72+
73+
def execute_command(command, stderr=subprocess.PIPE, cwd=None):
74+
"""
75+
Execute a given command and yield the messages from the standard output.
76+
77+
:param command: a list of strings which is the command to execute
78+
:param cwd: a PathLike object which is the working directory. Defaults to
79+
None
80+
:raises subprocess.CalledProcessError: if the process terminates with a
81+
non-zero return code
82+
:yield: a string holding a line of message in the standard output
83+
"""
84+
process = subprocess.Popen(
85+
command,
86+
bufsize=1,
87+
stdout=subprocess.PIPE,
88+
stderr=stderr,
89+
universal_newlines=True,
90+
cwd=cwd,
91+
)
92+
93+
line = ""
94+
while True:
95+
char = process.stdout.read(1)
96+
if char == "\n" or char == "\r":
97+
clear_the_last_line()
98+
yield line
99+
line = ""
100+
continue
101+
102+
elif char == "":
103+
break
104+
105+
line = line + char
106+
107+
process.stdout.close()
108+
return_code = process.wait()
109+
110+
if return_code:
111+
error_messages = ""
112+
if stderr == subprocess.PIPE:
113+
for message in process.stderr.readlines():
114+
error_messages = error_messages + message
115+
116+
raise subprocess.CalledProcessError(
117+
return_code, command, stderr=error_messages
118+
)
119+
120+
if stderr == subprocess.PIPE:
121+
process.stderr.close()
122+
123+
124+
def get_rizin_version(rizin_path) -> Str:
125+
"""
126+
Get the version number of the Rizin instance in the path.
127+
128+
:param rizin_path: a path to the Rizin executable
129+
:return: the version number of the Rizin instance
130+
"""
131+
try:
132+
process = subprocess.run(
133+
[rizin_path, "-v"], timeout=5, check=True, stdout=subprocess.PIPE
134+
)
135+
result = str(process.stdout)
136+
137+
matched_versions = re.finditer(
138+
r"[0-9]+\.[0-9]+\.[0-9]+", result[: result.index("@")]
139+
)
140+
first_matched = next(matched_versions, None)
141+
142+
if first_matched:
143+
return first_matched.group(0)
144+
else:
145+
return None
146+
147+
except BaseException:
148+
return None
149+
150+
151+
def download_rizin(target_path) -> Boolean:
152+
"""
153+
Download the source code of Rizin into the specified path. If a file or
154+
folder already exists, this function will remove them.
155+
156+
:param target_path: a PathLike object specifying the location to save the
157+
downloaded files
158+
:return: a boolean indicating if the operation finishes without errors
159+
"""
160+
if access(target_path, F_OK):
161+
shutil.rmtree(target_path)
162+
mkdir(target_path)
163+
164+
try:
165+
print()
166+
167+
for line in execute_command(
168+
[
169+
"git",
170+
"clone",
171+
"--progress",
172+
"https://github.com/rizinorg/rizin",
173+
target_path,
174+
],
175+
stderr=subprocess.STDOUT,
176+
):
177+
print_info(line)
178+
179+
return True
180+
181+
except subprocess.CalledProcessError as error:
182+
print_error("An error occurred when downloading Rizin.\n")
183+
184+
return False
185+
186+
187+
def update_rizin(source_path, target_commit) -> Boolean:
188+
"""
189+
Checkout the specified commit in the Rizin repository. Then, compile the
190+
source code to build a Rizin executable.
191+
192+
:param source_path: a PathLike object specifying the location to the
193+
source code
194+
:param target_commit: a hash value representing a valid commit in the
195+
repository
196+
:return: a boolean indicating the operation is success or not
197+
"""
198+
199+
try:
200+
print()
201+
202+
# Checkout to target commit
203+
for line in execute_command(
204+
["git", "checkout", target_commit], cwd=source_path
205+
):
206+
print_info(line)
207+
208+
# Remove the last build
209+
for line in execute_command(["rm", "-rf", "build"], cwd=source_path):
210+
print_info(line)
211+
212+
# Clean out old subproject
213+
for line in execute_command(
214+
["git", "clean", "-dxff", "subprojects/"], cwd=source_path
215+
):
216+
print_info(line)
217+
218+
except subprocess.CalledProcessError as error:
219+
print_error("An error occurred when updating Rizin.\n")
220+
221+
for line in error.stderr.decode().splitlines():
222+
print_error(line)
223+
224+
return False
225+
226+
# Compile Rizin
227+
try:
228+
print()
229+
230+
# Configure
231+
for line in execute_command(
232+
["meson", "--buildtype=release", "build"], cwd=source_path
233+
):
234+
print_info(line)
235+
236+
# Compile the source code
237+
for line in execute_command(
238+
["meson", "compile", "-C", "build"], cwd=source_path
239+
):
240+
print_info(line)
241+
242+
return True
243+
244+
except subprocess.CalledProcessError as error:
245+
print_error("An error occurred when updating Rizin.\n")
246+
247+
for line in error.stderr.decode().splitlines():
248+
print_error(line)
249+
250+
return False
251+
252+
253+
def find_rizin_instance(
254+
rizin_source_path: PathLike = RIZIN_DIR,
255+
target_commit: Str = RIZIN_COMMIT,
256+
disable_rizin_installation: Boolean = False,
257+
) -> Str:
258+
"""
259+
Search the system PATH and the Quark directory (~/.quark-engine)
260+
respectively to find an appropriate Rizin executable. If none of them are
261+
usable and the user doesn't disable the automatic installation feature,
262+
this method will download the source code and compile a Rizin executable
263+
for Quark.
264+
265+
:param rizin_source_path: a path to the source code of Rizin. Defaults to
266+
RIZIN_DIR
267+
:param target_commit: a commmit specifying the Rizin version to compile.
268+
Defaults to RIZIN_COMMIT
269+
:param disable_rizin_installation: a flag to disable the automatic
270+
installation of Rizin. Defaults to False
271+
:return: a path if an appropriate Rizin executable is found, otherwise
272+
None
273+
"""
274+
275+
# Search Rizin in PATH
276+
which_result = shutil.which("rizin")
277+
if which_result:
278+
version = get_rizin_version(which_result)
279+
if version in COMPATIBLE_RAZIN_VERSIONS:
280+
return which_result
281+
282+
# Otherwise, search the home path
283+
rizin_executable_path = rizin_source_path + "build/binrz/rizin/rizin"
284+
current_version = get_rizin_version(rizin_executable_path)
285+
286+
if not current_version and not disable_rizin_installation:
287+
print_info("Cannot find a compatible Rizin instance.")
288+
print_info("Automatically install Rizin into the Quark directory.")
289+
for _ in range(3):
290+
result = download_rizin(rizin_source_path)
291+
if result:
292+
break
293+
294+
result = update_rizin(rizin_source_path, target_commit)
295+
if result:
296+
print_success("Successfully install Rizin.")
297+
return rizin_executable_path
298+
else:
299+
return None
300+
301+
if not current_version:
302+
return None
303+
304+
if current_version in COMPATIBLE_RAZIN_VERSIONS:
305+
return rizin_executable_path
306+
307+
# The current version is not compatible
308+
print_info(
309+
"Find an outdated Rizin executable in the Quark directory. Try to update it."
310+
)
311+
312+
if disable_rizin_installation:
313+
return rizin_executable_path
314+
else:
315+
# Update and compile the source code
316+
result = update_rizin(rizin_source_path, target_commit)
317+
if result:
318+
return rizin_executable_path
319+
else:
320+
return None

0 commit comments

Comments
 (0)