1import array
2import os
3import shlex
4import shutil
5import subprocess
6import sys
7import time
8from xml.dom import minidom
9from xml.etree import ElementTree as ET
10
11import numpy as np
12from PyQt5.QtCore import Qt
13from PyQt5.QtGui import QFontDatabase, QFont
14from PyQt5.QtGui import QIcon
15from PyQt5.QtWidgets import QApplication, QSplitter
16from PyQt5.QtWidgets import QDialog, QVBoxLayout, QPlainTextEdit, QTableWidgetItem
17
18from urh import settings
19from urh.util.Logger import logger
20
21PROJECT_PATH = None  # for referencing in external program calls
22
23BCD_ERROR_SYMBOL = "?"
24BCD_LUT = {"{0:04b}".format(i): str(i) if i < 10 else BCD_ERROR_SYMBOL for i in range(16)}
25BCD_REVERSE_LUT = {str(i): "{0:04b}".format(i) for i in range(10)}
26BCD_REVERSE_LUT[BCD_ERROR_SYMBOL] = "0000"
27
28DEFAULT_PROGRAMS_WINDOWS = {}
29
30
31def profile(func):
32    def func_wrapper(*args):
33        t = time.perf_counter()
34        result = func(*args)
35        print("{} took {:.2f}ms".format(func, 1000 * (time.perf_counter() - t)))
36        return result
37
38    return func_wrapper
39
40
41def set_icon_theme():
42    if sys.platform != "linux" or settings.read("icon_theme_index", 0, int) == 0:
43        # noinspection PyUnresolvedReferences
44        import urh.ui.xtra_icons_rc
45        QIcon.setThemeName("oxy")
46    else:
47        QIcon.setThemeName("")
48
49
50def get_free_port():
51    import socket
52    s = socket.socket()
53    s.bind(("", 0))
54    port = s.getsockname()[1]
55    s.close()
56    return port
57
58
59def set_shared_library_path():
60    shared_lib_dir = get_shared_library_path()
61
62    if shared_lib_dir:
63
64        if sys.platform == "win32":
65            current_path = os.environ.get("PATH", '')
66            if not current_path.startswith(shared_lib_dir):
67                os.environ["PATH"] = shared_lib_dir + os.pathsep + current_path
68        else:
69            # LD_LIBRARY_PATH will not be considered at runtime so we explicitly load the .so's we need
70            exts = [".so"] if sys.platform == "linux" else [".so", ".dylib"]
71            import ctypes
72            libs = sorted(os.listdir(shared_lib_dir))
73            libusb = next((lib for lib in libs if "libusb" in lib), None)
74            if libusb:
75                # Ensure libusb is loaded first
76                libs.insert(0, libs.pop(libs.index(libusb)))
77
78            for lib in libs:
79                if lib.lower().startswith("lib") and any(ext in lib for ext in exts):
80                    lib_path = os.path.join(shared_lib_dir, lib)
81                    if os.path.isfile(lib_path):
82                        try:
83                            ctypes.cdll.LoadLibrary(lib_path)
84                        except Exception as e:
85                            logger.exception(e)
86
87
88def get_shared_library_path():
89    if hasattr(sys, "frozen"):
90        return os.path.dirname(sys.executable)
91
92    util_dir = os.path.dirname(os.path.realpath(__file__)) if not os.path.islink(__file__) \
93        else os.path.dirname(os.path.realpath(os.readlink(__file__)))
94    urh_dir = os.path.realpath(os.path.join(util_dir, ".."))
95    assert os.path.isdir(urh_dir)
96
97    shared_lib_dir = os.path.realpath(os.path.join(urh_dir, "dev", "native", "lib", "shared"))
98    if os.path.isdir(shared_lib_dir):
99        return shared_lib_dir
100    else:
101        return ""
102
103
104def convert_bits_to_string(bits, output_view_type: int, pad_zeros=False, lsb=False, lsd=False, endianness="big"):
105    """
106    Convert bit array to string
107    :param endianness: Endianness little or big
108    :param bits: Bit array
109    :param output_view_type: Output view type index
110    0 = bit, 1=hex, 2=ascii, 3=decimal 4=binary coded decimal (bcd)
111    :param pad_zeros:
112    :param lsb: Least Significant Bit   -> Reverse bits first
113    :param lsd: Least Significant Digit -> Reverse result at end
114    :return:
115    """
116    bits_str = "".join(["1" if b else "0" for b in bits])
117
118    if output_view_type == 4:
119        # For BCD we need to enforce padding
120        pad_zeros = True
121
122    if pad_zeros and output_view_type in (1, 2, 4):
123        n = 4 if output_view_type in (1, 4) else 8 if output_view_type == 2 else 1
124        bits_str += "0" * ((n - (len(bits_str) % n)) % n)
125
126    if lsb:
127        # Reverse bit string
128        bits_str = bits_str[::-1]
129
130    if endianness == "little":
131        # reverse byte wise
132        bits_str = "".join(bits_str[max(i - 8, 0):i] for i in range(len(bits_str), 0, -8))
133
134    if output_view_type == 0:  # bit
135        result = bits_str
136
137    elif output_view_type == 1:  # hex
138        result = "".join(["{0:x}".format(int(bits_str[i:i + 4], 2)) for i in range(0, len(bits_str), 4)])
139
140    elif output_view_type == 2:  # ascii
141        result = "".join(map(chr,
142                             [int("".join(bits_str[i:i + 8]), 2) for i in range(0, len(bits_str), 8)]))
143
144    elif output_view_type == 3:  # decimal
145        try:
146            result = str(int(bits_str, 2))
147        except ValueError:
148            return None
149    elif output_view_type == 4:  # bcd
150        result = "".join([BCD_LUT[bits_str[i:i + 4]] for i in range(0, len(bits_str), 4)])
151    else:
152        raise ValueError("Unknown view type")
153
154    if lsd:
155        # reverse result
156        return result[::-1]
157    else:
158        return result
159
160
161def hex2bit(hex_str: str) -> array.array:
162    if not isinstance(hex_str, str):
163        return array.array("B", [])
164
165    if hex_str[:2] == "0x":
166        hex_str = hex_str[2:]
167
168    try:
169        bitstring = "".join("{0:04b}".format(int(h, 16)) for h in hex_str)
170        return array.array("B", [True if x == "1" else False for x in bitstring])
171    except (TypeError, ValueError) as e:
172        logger.error(e)
173        result = array.array("B", [])
174
175    return result
176
177
178def ascii2bit(ascii_str: str) -> array.array:
179    if not isinstance(ascii_str, str):
180        return array.array("B", [])
181
182    try:
183        bitstring = "".join("{0:08b}".format(ord(c)) for c in ascii_str)
184        return array.array("B", [True if x == "1" else False for x in bitstring])
185    except (TypeError, ValueError) as e:
186        logger.error(e)
187        result = array.array("B", [])
188
189    return result
190
191
192def decimal2bit(number: str, num_bits: int) -> array.array:
193    try:
194        number = int(number)
195    except ValueError as e:
196        logger.error(e)
197        return array.array("B", [])
198
199    fmt_str = "{0:0" + str(num_bits) + "b}"
200    return array.array("B", map(int, fmt_str.format(number)))
201
202
203def bcd2bit(value: str) -> array.array:
204    try:
205        return array.array("B", map(int, "".join(BCD_REVERSE_LUT[c] for c in value)))
206    except Exception as e:
207        logger.error(e)
208        return array.array("B", [])
209
210
211def convert_string_to_bits(value: str, display_format: int, target_num_bits: int) -> array.array:
212    if display_format == 0:
213        result = string2bits(value)
214    elif display_format == 1:
215        result = hex2bit(value)
216    elif display_format == 2:
217        result = ascii2bit(value)
218    elif display_format == 3:
219        result = decimal2bit(value, target_num_bits)
220    elif display_format == 4:
221        result = bcd2bit(value)
222    else:
223        raise ValueError("Unknown display format {}".format(display_format))
224
225    if len(result) == 0:
226        raise ValueError("Error during conversion.")
227
228    if len(result) < target_num_bits:
229        # pad with zeros
230        return result + array.array("B", [0] * (target_num_bits - len(result)))
231    else:
232        return result[:target_num_bits]
233
234
235def create_textbox_dialog(content: str, title: str, parent) -> QDialog:
236    d = QDialog(parent)
237    d.resize(800, 600)
238    d.setWindowTitle(title)
239    layout = QVBoxLayout(d)
240    text_edit = QPlainTextEdit(content)
241    text_edit.setReadOnly(True)
242    layout.addWidget(text_edit)
243    d.setLayout(layout)
244    return d
245
246
247def string2bits(bit_str: str) -> array.array:
248    return array.array("B", map(int, bit_str))
249
250
251def bit2hex(bits: array.array, pad_zeros=False) -> str:
252    return convert_bits_to_string(bits, 1, pad_zeros)
253
254
255def number_to_bits(n: int, length: int) -> array.array:
256    fmt = "{0:0" + str(length) + "b}"
257    return array.array("B", map(int, fmt.format(n)))
258
259
260def bits_to_number(bits: array.array) -> int:
261    return int("".join(map(str, bits)), 2)
262
263
264def aggregate_bits(bits: array.array, size=4) -> array.array:
265    result = array.array("B", [])
266
267    for i in range(0, len(bits), size):
268        h = 0
269        for k in range(size):
270            try:
271                h += (2 ** (size - 1 - k)) * bits[i + k]
272            except IndexError:
273                # Implicit padding with zeros
274                continue
275        result.append(h)
276
277    return result
278
279
280def convert_numbers_to_hex_string(arr: np.ndarray):
281    """
282    Convert an array like [0, 1, 10, 2] to string 012a2
283
284    :param arr:
285    :return:
286    """
287    lut = {i: "{0:x}".format(i) for i in range(16)}
288    return "".join(lut[x] if x in lut else " {} ".format(x) for x in arr)
289
290
291def clip(value, minimum, maximum):
292    return max(minimum, min(value, maximum))
293
294
295def file_can_be_opened(filename: str):
296    try:
297        open(filename, "r").close()
298        return True
299    except Exception as e:
300        if not isinstance(e, FileNotFoundError):
301            logger.debug(str(e))
302        return False
303
304
305def create_table_item(content):
306    item = QTableWidgetItem(str(content))
307    item.setFlags(item.flags() & ~Qt.ItemIsEditable)
308    return item
309
310
311def write_xml_to_file(xml_tag: ET.Element, filename: str):
312    xml_str = minidom.parseString(ET.tostring(xml_tag)).toprettyxml(indent="  ")
313    with open(filename, "w") as f:
314        for line in xml_str.split("\n"):
315            if line.strip():
316                f.write(line + "\n")
317
318
319def get_monospace_font() -> QFont:
320    fixed_font = QFontDatabase.systemFont(QFontDatabase.FixedFont)
321    fixed_font.setPointSize(QApplication.instance().font().pointSize())
322    return fixed_font
323
324
325def get_name_from_filename(filename: str):
326    if not isinstance(filename, str):
327        return "No Name"
328
329    return os.path.basename(filename).split(".")[0]
330
331
332def get_default_windows_program_for_extension(extension: str):
333    if os.name != "nt":
334        return None
335
336    if not extension.startswith("."):
337        extension = "." + extension
338
339    if extension in DEFAULT_PROGRAMS_WINDOWS:
340        return DEFAULT_PROGRAMS_WINDOWS[extension]
341
342    try:
343        assoc = subprocess.check_output("assoc " + extension, shell=True, stderr=subprocess.PIPE).decode().split("=")[1]
344        ftype = subprocess.check_output("ftype " + assoc, shell=True).decode().split("=")[1].split(" ")[0]
345        ftype = ftype.replace('"', '')
346        assert shutil.which(ftype) is not None
347    except Exception:
348        return None
349
350    DEFAULT_PROGRAMS_WINDOWS[extension] = ftype
351    return ftype
352
353
354def parse_command(command: str):
355    try:
356        posix = os.name != "nt"
357        splitted = shlex.split(command, posix=posix)
358        # strip quotations
359        if not posix:
360            splitted = [s.replace('"', '').replace("'", "") for s in splitted]
361    except ValueError:
362        splitted = []  # e.g. when missing matching "
363
364    if len(splitted) == 0:
365        return "", []
366
367    cmd = splitted.pop(0)
368    if PROJECT_PATH is not None and not os.path.isabs(cmd) and shutil.which(cmd) is None:
369        # Path relative to project path
370        cmd = os.path.normpath(os.path.join(PROJECT_PATH, cmd))
371    cmd = [cmd]
372
373    # This is for legacy support, if you have filenames with spaces and did not quote them
374    while shutil.which(" ".join(cmd)) is None and len(splitted) > 0:
375        cmd.append(splitted.pop(0))
376
377    return " ".join(cmd), splitted
378
379
380def run_command(command, param: str = None, use_stdin=False, detailed_output=False, return_rc=False):
381    cmd, arg = parse_command(command)
382    if shutil.which(cmd) is None:
383        logger.error("Could not find {}".format(cmd))
384        return ""
385
386    startupinfo = None
387    if os.name == 'nt':
388        startupinfo = subprocess.STARTUPINFO()
389        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
390        if "." in cmd:
391            default_app = get_default_windows_program_for_extension(cmd.split(".")[-1])
392            if default_app:
393                arg.insert(0, cmd)
394                cmd = default_app
395
396    call_list = [cmd] + arg
397    try:
398        if detailed_output:
399            if param is not None:
400                call_list.append(param)
401
402            p = subprocess.Popen(call_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo)
403            out, err = p.communicate()
404            result = "{} exited with {}".format(" ".join(call_list), p.returncode)
405            if out.decode():
406                result += " stdout: {}".format(out.decode())
407            if err.decode():
408                result += " stderr: {}".format(err.decode())
409
410            if return_rc:
411                return result, p.returncode
412            else:
413                return result
414        elif use_stdin:
415            p = subprocess.Popen(call_list, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE,
416                                 startupinfo=startupinfo)
417            param = param.encode() if param is not None else None
418            out, _ = p.communicate(param)
419            if return_rc:
420                return out.decode(), p.returncode
421            else:
422                return out.decode()
423        else:
424            if param is not None:
425                call_list.append(param)
426
427            if return_rc:
428                raise ValueError("Return Code not supported for this configuration")
429
430            return subprocess.check_output(call_list, stderr=subprocess.PIPE, startupinfo=startupinfo).decode()
431    except Exception as e:
432        msg = "Could not run {} ({})".format(cmd, e)
433        logger.error(msg)
434        if detailed_output:
435            return msg
436        else:
437            return ""
438
439
440def validate_command(command: str):
441    if not isinstance(command, str):
442        return False
443
444    cmd, _ = parse_command(command)
445    return shutil.which(cmd) is not None
446
447
448def set_splitter_stylesheet(splitter: QSplitter):
449    splitter.setHandleWidth(4)
450    bgcolor = settings.BGCOLOR.lighter(150)
451    r, g, b, a = bgcolor.red(), bgcolor.green(), bgcolor.blue(), bgcolor.alpha()
452    splitter.setStyleSheet("QSplitter::handle:vertical {{margin: 4px 0px; "
453                           "background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
454                           "stop:0.2 rgba(255, 255, 255, 0),"
455                           "stop:0.5 rgba({0}, {1}, {2}, {3}),"
456                           "stop:0.8 rgba(255, 255, 255, 0));"
457                           "image: url(:/icons/icons/splitter_handle_horizontal.svg);}}"
458                           "QSplitter::handle:horizontal {{margin: 4px 0px; "
459                           "background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, "
460                           "stop:0.2 rgba(255, 255, 255, 0),"
461                           "stop:0.5 rgba({0}, {1}, {2}, {3}),"
462                           "stop:0.8 rgba(255, 255, 255, 0));"
463                           "image: url(:/icons/icons/splitter_handle_vertical.svg);}}".format(r, g, b, a))
464
465
466def calc_x_y_scale(rect, parent):
467    view_rect = parent.view_rect() if hasattr(parent, "view_rect") else rect
468    parent_width = parent.width() if hasattr(parent, "width") else 750
469    parent_height = parent.height() if hasattr(parent, "height") else 300
470
471    scale_x = view_rect.width() / parent_width
472    scale_y = view_rect.height() / parent_height
473
474    return scale_x, scale_y
475