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