util.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. util: Frequently-used variables, classes and utility functions for the MMGen suite
  20. """
  21. import sys, os, time, re
  22. from .color import red, yellow, green, blue, purple
  23. from .cfg import gv
  24. ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz'
  25. digits = '0123456789'
  26. hexdigits = '0123456789abcdefABCDEF'
  27. hexdigits_uc = '0123456789ABCDEF'
  28. hexdigits_lc = '0123456789abcdef'
  29. def noop(*args, **kwargs):
  30. pass
  31. class Util:
  32. def __init__(self, cfg):
  33. self.cfg = cfg
  34. if cfg.quiet:
  35. self.qmsg = self.qmsg_r = noop
  36. else:
  37. self.qmsg = msg
  38. self.qmsg_r = msg_r
  39. if cfg.verbose:
  40. self.vmsg = msg
  41. self.vmsg_r = msg_r
  42. self.Vmsg = Msg
  43. self.Vmsg_r = Msg_r
  44. else:
  45. self.vmsg = self.vmsg_r = self.Vmsg = self.Vmsg_r = noop
  46. self.dmsg = msg if cfg.debug else noop
  47. if cfg.pager:
  48. from .ui import do_pager
  49. self.stdout_or_pager = do_pager
  50. else:
  51. self.stdout_or_pager = Msg_r
  52. def compare_chksums(
  53. self,
  54. chk1,
  55. desc1,
  56. chk2,
  57. desc2,
  58. hdr = '',
  59. die_on_fail = False,
  60. verbose = False):
  61. if not chk1 == chk2:
  62. fs = "{} ERROR: {} checksum ({}) doesn't match {} checksum ({})"
  63. m = fs.format((hdr+':\n ' if hdr else 'CHECKSUM'), desc2, chk2, desc1, chk1)
  64. if die_on_fail:
  65. die(3, m)
  66. else:
  67. if verbose or self.cfg.verbose:
  68. msg(m)
  69. return False
  70. if self.cfg.verbose:
  71. msg(f'{capfirst(desc1)} checksum OK ({chk1})')
  72. return True
  73. def compare_or_die(self, val1, desc1, val2, desc2, e='Error'):
  74. if val1 != val2:
  75. die(3, f"{e}: {desc2} ({val2}) doesn't match {desc1} ({val1})")
  76. if self.cfg.debug:
  77. msg(f'{capfirst(desc2)} OK ({val2})')
  78. return True
  79. if sys.platform == 'win32':
  80. def msg_r(s):
  81. try:
  82. gv.stderr.write(s)
  83. gv.stderr.flush()
  84. except:
  85. os.write(2, s.encode())
  86. def msg(s):
  87. msg_r(s + '\n')
  88. def Msg_r(s):
  89. try:
  90. gv.stdout.write(s)
  91. gv.stdout.flush()
  92. except:
  93. os.write(1, s.encode())
  94. def Msg(s):
  95. Msg_r(s + '\n')
  96. else:
  97. def msg(s):
  98. gv.stderr.write(s + '\n')
  99. def msg_r(s):
  100. gv.stderr.write(s)
  101. gv.stderr.flush()
  102. def Msg(s):
  103. gv.stdout.write(s + '\n')
  104. def Msg_r(s):
  105. gv.stdout.write(s)
  106. gv.stdout.flush()
  107. def rmsg(s):
  108. msg(red(s))
  109. def ymsg(s):
  110. msg(yellow(s))
  111. def gmsg(s):
  112. msg(green(s))
  113. def gmsg_r(s):
  114. msg_r(green(s))
  115. def bmsg(s):
  116. msg(blue(s))
  117. def pumsg(s):
  118. msg(purple(s))
  119. def mmsg(*args):
  120. for d in args:
  121. Msg(repr(d))
  122. def mdie(*args):
  123. mmsg(*args)
  124. sys.exit(0)
  125. def die(ev, s='', stdout=False):
  126. if isinstance(ev, int):
  127. from .exception import MMGenSystemExit, MMGenError
  128. if ev <= 2:
  129. raise MMGenSystemExit(ev, s, stdout)
  130. else:
  131. raise MMGenError(ev, s, stdout)
  132. elif isinstance(ev, str):
  133. from . import exception
  134. raise getattr(exception, ev)(s)
  135. else:
  136. raise ValueError(f'{ev}: exit value must be string or int instance')
  137. def Die(ev=0, s=''):
  138. die(ev=ev, s=s, stdout=True)
  139. def pp_fmt(d):
  140. import pprint
  141. return pprint.PrettyPrinter(indent=4, compact=False).pformat(d)
  142. def pp_msg(d):
  143. msg(pp_fmt(d))
  144. def indent(s, indent=' ', append='\n'):
  145. "indent multiple lines of text with specified string"
  146. return indent + ('\n'+indent).join(s.strip().splitlines()) + append
  147. def fmt(s, indent='', strip_char=None, append='\n'):
  148. "de-indent multiple lines of text, or indent with specified string"
  149. return indent + ('\n'+indent).join([l.lstrip(strip_char) for l in s.strip().splitlines()]) + append
  150. def fmt_list(iterable, fmt='dfl', indent='', conv=None):
  151. "pretty-format a list"
  152. _conv, sep, lq, rq = {
  153. 'dfl': (str, ", ", "'", "'"),
  154. 'utf8': (str, ", ", "“", "”"),
  155. 'bare': (repr, " ", "", ""),
  156. 'barest': (str, " ", "", ""),
  157. 'fancy': (str, " ", "‘", "’"),
  158. 'no_quotes': (str, ", ", "", ""),
  159. 'compact': (str, ",", "", ""),
  160. 'no_spc': (str, ",", "'", "'"),
  161. 'min': (str, ",", "", ""),
  162. 'repr': (repr, ", ", "", ""),
  163. 'csv': (repr, ",", "", ""),
  164. 'col': (str, "\n", "", ""),
  165. }[fmt]
  166. conv = conv or _conv
  167. return indent + (sep+indent).join(lq+conv(e)+rq for e in iterable)
  168. def fmt_dict(mapping, fmt='dfl', kconv=None, vconv=None):
  169. "pretty-format a dict"
  170. kc, vc, sep, fs = {
  171. 'dfl': (str, str, ", ", "'{}' ({})"),
  172. 'dfl_compact': (str, str, " ", "{} ({})"),
  173. 'square': (str, str, ", ", "'{}' [{}]"),
  174. 'square_compact':(str, str, " ", "{} [{}]"),
  175. 'equal': (str, str, ", ", "'{}'={}"),
  176. 'equal_spaced': (str, str, ", ", "'{}' = {}"),
  177. 'equal_compact': (str, str, " ", "{}={}"),
  178. 'kwargs': (str, repr, ", ", "{}={}"),
  179. 'colon': (str, repr, ", ", "{}:{}"),
  180. 'colon_compact': (str, str, " ", "{}:{}"),
  181. }[fmt]
  182. kconv = kconv or kc
  183. vconv = vconv or vc
  184. return sep.join(fs.format(kconv(k), vconv(v)) for k, v in mapping.items())
  185. def list_gen(*data):
  186. """
  187. Generate a list from an arg tuple of sublists
  188. - The last element of each sublist is a condition. If it evaluates to true, the preceding
  189. elements of the sublist are included in the result. Otherwise the sublist is skipped.
  190. - If a sublist contains only one element, the condition defaults to true.
  191. """
  192. assert type(data) in (list, tuple), f'{type(data).__name__} not in (list, tuple)'
  193. def gen():
  194. for d in data:
  195. assert isinstance(d, list), f'{type(d).__name__} != list'
  196. if len(d) == 1:
  197. yield d[0]
  198. elif d[-1]:
  199. for idx in range(len(d)-1):
  200. yield d[idx]
  201. return list(gen())
  202. def remove_dups(iterable, edesc='element', desc='list', quiet=False, hide=False):
  203. """
  204. Remove duplicate occurrences of iterable elements, preserving first occurrence
  205. If iterable is a generator, return a list, else type(iterable)
  206. """
  207. ret = []
  208. for e in iterable:
  209. if e in ret:
  210. if not quiet:
  211. ymsg(f'Warning: removing duplicate {edesc} {"(hidden)" if hide else e} in {desc}')
  212. else:
  213. ret.append(e)
  214. return ret if type(iterable).__name__ == 'generator' else type(iterable)(ret)
  215. def contains_any(target_list, source_list):
  216. return any(map(target_list.count, source_list))
  217. def suf(arg, suf_type='s', verb='none'):
  218. suf_types = {
  219. 'none': {
  220. 's': ('s', ''),
  221. 'es': ('es', ''),
  222. 'ies': ('ies', 'y'),
  223. },
  224. 'is': {
  225. 's': ('s are', ' is'),
  226. 'es': ('es are', ' is'),
  227. 'ies': ('ies are', 'y is'),
  228. },
  229. 'has': {
  230. 's': ('s have', ' has'),
  231. 'es': ('es have', ' has'),
  232. 'ies': ('ies have', 'y has'),
  233. },
  234. }
  235. if isinstance(arg, int):
  236. n = arg
  237. elif isinstance(arg, (list, tuple, set, dict)):
  238. n = len(arg)
  239. else:
  240. die(2, f'{arg}: invalid parameter for suf()')
  241. return suf_types[verb][suf_type][n == 1]
  242. def get_extension(fn):
  243. return os.path.splitext(fn)[1][1:]
  244. def remove_extension(fn, ext):
  245. a, b = os.path.splitext(fn)
  246. return a if b[1:] == ext else fn
  247. def make_chksum_N(s, nchars, sep=False, rounds=2, upper=True):
  248. if isinstance(s, str):
  249. s = s.encode()
  250. from hashlib import sha256
  251. for i in range(rounds):
  252. s = sha256(s).digest()
  253. ret = s.hex()[:nchars]
  254. if sep:
  255. assert 4 <= nchars <= 64 and (not nchars % 4), 'illegal ‘nchars’ value'
  256. ret = ' '.join(ret[i:i+4] for i in range(0, nchars, 4))
  257. else:
  258. assert 4 <= nchars <= 64, 'illegal ‘nchars’ value'
  259. return ret.upper() if upper else ret
  260. def make_chksum_8(s, sep=False):
  261. from .obj import HexStr
  262. from hashlib import sha256
  263. s = HexStr(sha256(sha256(s).digest()).hexdigest()[:8].upper(), case='upper')
  264. return '{} {}'.format(s[:4], s[4:]) if sep else s
  265. def make_chksum_6(s):
  266. from .obj import HexStr
  267. from hashlib import sha256
  268. if isinstance(s, str):
  269. s = s.encode()
  270. return HexStr(sha256(s).hexdigest()[:6])
  271. def is_chksum_6(s):
  272. return len(s) == 6 and set(s) <= set(hexdigits_lc)
  273. def split_into_cols(col_wid, s):
  274. return ' '.join([s[col_wid*i:col_wid*(i+1)] for i in range(len(s)//col_wid+1)]).rstrip()
  275. def capfirst(s): # different from str.capitalize() - doesn't downcase any uc in string
  276. return s if len(s) == 0 else s[0].upper() + s[1:]
  277. def decode_timestamp(s):
  278. # tz_save = open('/etc/timezone').read().rstrip()
  279. os.environ['TZ'] = 'UTC'
  280. # os.environ['TZ'] = tz_save
  281. return int(time.mktime(time.strptime(s, '%Y%m%d_%H%M%S')))
  282. def make_timestamp(secs=None):
  283. return '{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}'.format(*time.gmtime(
  284. int(secs) if secs is not None else time.time())[:6])
  285. def make_timestr(secs=None):
  286. return '{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}'.format(*time.gmtime(
  287. int(secs) if secs is not None else time.time())[:6])
  288. def secs_to_dhms(secs):
  289. hrs = secs // 3600
  290. return '{}{:02d}:{:02d}:{:02d} h/m/s'.format(
  291. ('{} day{}, '.format(hrs//24, suf(hrs//24)) if hrs > 24 else ''),
  292. hrs % 24,
  293. (secs // 60) % 60,
  294. secs % 60
  295. )
  296. def secs_to_hms(secs):
  297. return '{:02d}:{:02d}:{:02d}'.format(secs//3600, (secs//60) % 60, secs % 60)
  298. def secs_to_ms(secs):
  299. return '{:02d}:{:02d}'.format(secs//60, secs % 60)
  300. def is_int(s): # actually is_nonnegative_int()
  301. return set(str(s)) <= set(digits)
  302. def check_int_between(val, imin, imax, desc):
  303. if not imin <= int(val) <= imax:
  304. die(1, f'{val}: invalid value for {desc} (must be between {imin} and {imax})')
  305. return int(val)
  306. def is_hex_str(s):
  307. return set(s) <= set(hexdigits)
  308. def is_hex_str_lc(s):
  309. return set(s) <= set(hexdigits_lc)
  310. def is_utf8(s):
  311. try:
  312. s.decode('utf8')
  313. except:
  314. return False
  315. else:
  316. return True
  317. def remove_whitespace(s, ws='\t\r\n '):
  318. return s.translate(dict((ord(e), None) for e in ws))
  319. def strip_comment(line):
  320. return re.sub('#.*', '', line).rstrip()
  321. def strip_comments(lines):
  322. pat = re.compile('#.*')
  323. return [m for m in [pat.sub('', l).rstrip() for l in lines] if m != '']
  324. def make_full_path(outdir, outfile):
  325. return os.path.normpath(os.path.join(outdir, os.path.basename(outfile)))
  326. class oneshot_warning:
  327. color = 'nocolor'
  328. def __init__(self, div=None, fmt_args=[], reverse=False):
  329. self.do(type(self), div, fmt_args, reverse)
  330. def do(self, wcls, div, fmt_args, reverse):
  331. def do_warning():
  332. from . import color
  333. msg(getattr(color, getattr(wcls, 'color'))('WARNING: ' + getattr(wcls, 'message').format(*fmt_args)))
  334. if not hasattr(wcls, 'data'):
  335. setattr(wcls, 'data', [])
  336. data = getattr(wcls, 'data')
  337. condition = (div in data) if reverse else (not div in data)
  338. if not div in data:
  339. data.append(div)
  340. if condition:
  341. do_warning()
  342. self.warning_shown = True
  343. else:
  344. self.warning_shown = False
  345. class oneshot_warning_group(oneshot_warning):
  346. def __init__(self, wcls, div=None, fmt_args=[], reverse=False):
  347. self.do(getattr(self, wcls), div, fmt_args, reverse)
  348. def get_subclasses(cls, names=False):
  349. def gen(cls):
  350. for i in cls.__subclasses__():
  351. yield i
  352. yield from gen(i)
  353. return tuple((c.__name__ for c in gen(cls)) if names else gen(cls))
  354. def async_run(coro):
  355. import asyncio
  356. return asyncio.run(coro)
  357. def wrap_ripemd160(called=[]):
  358. if not called:
  359. try:
  360. import hashlib
  361. hashlib.new('ripemd160')
  362. except ValueError:
  363. def hashlib_new_wrapper(name, *args, **kwargs):
  364. if name == 'ripemd160':
  365. return ripemd160(*args, **kwargs)
  366. else:
  367. return hashlib_new(name, *args, **kwargs)
  368. from .contrib.ripemd160 import ripemd160
  369. hashlib_new = hashlib.new
  370. hashlib.new = hashlib_new_wrapper
  371. called.append(True)
  372. def exit_if_mswin(feature):
  373. if sys.platform == 'win32':
  374. die(2, capfirst(feature) + ' not supported on the MSWin / MSYS2 platform')
  375. def have_sudo(silent=False):
  376. from subprocess import run, DEVNULL
  377. redir = DEVNULL if silent else None
  378. try:
  379. run(['sudo', '--non-interactive', 'true'], stdout=redir, stderr=redir, check=True)
  380. return True
  381. except:
  382. return False