util.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2021 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.py: Low-level routines imported by other modules in the MMGen suite
  20. """
  21. import sys,os,time,stat,re
  22. from subprocess import run,PIPE,DEVNULL
  23. from hashlib import sha256
  24. from string import hexdigits,digits
  25. from .color import *
  26. from .exception import *
  27. from .globalvars import *
  28. CUR_HIDE = '\033[?25l'
  29. CUR_SHOW = '\033[?25h'
  30. if g.platform == 'win':
  31. def msg_r(s):
  32. try:
  33. g.stderr.write(s)
  34. g.stderr.flush()
  35. except:
  36. os.write(2,s.encode())
  37. def Msg_r(s):
  38. try:
  39. g.stdout.write(s)
  40. g.stdout.flush()
  41. except:
  42. os.write(1,s.encode())
  43. def msg(s): msg_r(s + '\n')
  44. def Msg(s): Msg_r(s + '\n')
  45. else:
  46. def msg_r(s):
  47. g.stderr.write(s)
  48. g.stderr.flush()
  49. def Msg_r(s):
  50. g.stdout.write(s)
  51. g.stdout.flush()
  52. def msg(s): g.stderr.write(s + '\n')
  53. def Msg(s): g.stdout.write(s + '\n')
  54. def msgred(s): msg(red(s))
  55. def rmsg(s): msg(red(s))
  56. def rmsg_r(s): msg_r(red(s))
  57. def ymsg(s): msg(yellow(s))
  58. def ymsg_r(s): msg_r(yellow(s))
  59. def gmsg(s): msg(green(s))
  60. def gmsg_r(s): msg_r(green(s))
  61. def bmsg(s): msg(blue(s))
  62. def bmsg_r(s): msg_r(blue(s))
  63. def mmsg(*args):
  64. for d in args: Msg(repr(d))
  65. def mdie(*args):
  66. mmsg(*args); sys.exit(0)
  67. def die_wait(delay,ev=0,s=''):
  68. assert isinstance(delay,int)
  69. assert isinstance(ev,int)
  70. if s: msg(s)
  71. time.sleep(delay)
  72. sys.exit(ev)
  73. def die_pause(ev=0,s=''):
  74. assert isinstance(ev,int)
  75. if s: msg(s)
  76. input('Press ENTER to exit')
  77. sys.exit(ev)
  78. def die(ev=0,s=''):
  79. assert isinstance(ev,int)
  80. if s: msg(s)
  81. sys.exit(ev)
  82. def Die(ev=0,s=''):
  83. assert isinstance(ev,int)
  84. if s: Msg(s)
  85. sys.exit(ev)
  86. def rdie(ev=0,s=''): die(ev,red(s))
  87. def ydie(ev=0,s=''): die(ev,yellow(s))
  88. def pp_fmt(d):
  89. import pprint
  90. return pprint.PrettyPrinter(indent=4,compact=True).pformat(d)
  91. def pp_msg(d):
  92. msg(pp_fmt(d))
  93. def fmt(s,indent='',strip_char=None):
  94. "de-indent multiple lines of text, or indent with specified string"
  95. return indent + ('\n'+indent).join([l.strip(strip_char) for l in s.strip().splitlines()]) + '\n'
  96. def fmt_list(l,fmt='dfl',indent=''):
  97. "pretty-format a list"
  98. sep,lq,rq = {
  99. 'utf8': ("“, ”", "“", "”"),
  100. 'dfl': ("', '", "'", "'"),
  101. 'bare': (' ', '', '' ),
  102. 'no_quotes': (', ', '', '' ),
  103. 'no_spc': ("','", "'", "'"),
  104. 'min': (",", "'", "'"),
  105. 'col': ('\n'+indent, indent, '' ),
  106. }[fmt]
  107. return lq + sep.join(l) + rq
  108. def list_gen(*data):
  109. """
  110. add element to list if condition is true or absent
  111. """
  112. assert type(data) in (list,tuple), f'{type(data).__name__} not in (list,tuple)'
  113. def gen():
  114. for i in data:
  115. assert type(i) == list, f'{type(i).__name__} != list'
  116. assert len(i) in (1,2), f'{len(i)} not in (1,2)'
  117. if len(i) == 1 or i[1]:
  118. yield i[0]
  119. return list(gen())
  120. def remove_dups(iterable,edesc='element',desc='list',quiet=False,hide=False):
  121. """
  122. Remove duplicate occurrences of iterable elements, preserving first occurrence
  123. If iterable is a generator, return a list, else type(iterable)
  124. """
  125. ret = []
  126. for e in iterable:
  127. if e in ret:
  128. if not quiet:
  129. ymsg(f'Warning: removing duplicate {edesc} {"(hidden)" if hide else e} in {desc}')
  130. else:
  131. ret.append(e)
  132. return ret if type(iterable).__name__ == 'generator' else type(iterable)(ret)
  133. def exit_if_mswin(feature):
  134. if g.platform == 'win':
  135. m = capfirst(feature) + ' not supported on the MSWin / MSYS2 platform'
  136. ydie(1,m)
  137. def warn_altcoins(coinsym,trust_level):
  138. if trust_level > 3:
  139. return
  140. tl_str = (
  141. red('COMPLETELY UNTESTED'),
  142. red('LOW'),
  143. yellow('MEDIUM'),
  144. green('HIGH'),
  145. )[trust_level]
  146. m = """
  147. Support for coin {!r} is EXPERIMENTAL. The {pn} project
  148. assumes no responsibility for any loss of funds you may incur.
  149. This coin’s {pn} testing status: {}
  150. Are you sure you want to continue?
  151. """
  152. m = fmt(m).strip().format(coinsym.upper(),tl_str,pn=g.proj_name)
  153. if g.test_suite:
  154. qmsg(m)
  155. return
  156. if not keypress_confirm(m,default_yes=True):
  157. sys.exit(0)
  158. def set_for_type(val,refval,desc,invert_bool=False,src=None):
  159. if type(refval) == bool:
  160. v = str(val).lower()
  161. ret = (
  162. True if v in ('true','yes','1','on') else
  163. False if v in ('false','no','none','0','off','') else
  164. None
  165. )
  166. if ret is not None:
  167. return not ret if invert_bool else ret
  168. else:
  169. try:
  170. return type(refval)(not val if invert_bool else val)
  171. except:
  172. pass
  173. die(1,'{!r}: invalid value for {!r}{} (must be of type {!r})'.format(
  174. val,
  175. desc,
  176. ' in {!r}'.format(src) if src else '',
  177. type(refval).__name__) )
  178. # From 'man dd':
  179. # c=1, w=2, b=512, kB=1000, K=1024, MB=1000*1000, M=1024*1024,
  180. # GB=1000*1000*1000, G=1024*1024*1024, and so on for T, P, E, Z, Y.
  181. bytespec_map = (
  182. ('c', 1),
  183. ('w', 2),
  184. ('b', 512),
  185. ('kB', 1000),
  186. ('K', 1024),
  187. ('MB', 1000000),
  188. ('M', 1048576),
  189. ('GB', 1000000000),
  190. ('G', 1073741824),
  191. ('TB', 1000000000000),
  192. ('T', 1099511627776),
  193. ('PB', 1000000000000000),
  194. ('P', 1125899906842624),
  195. ('EB', 1000000000000000000),
  196. ('E', 1152921504606846976),
  197. )
  198. def int2bytespec(n,spec,fmt,print_sym=True):
  199. def spec2int(spec):
  200. for k,v in bytespec_map:
  201. if k == spec:
  202. return v
  203. else:
  204. die('{spec}: unrecognized bytespec')
  205. return '{:{}f}{}'.format( n / spec2int(spec), fmt, spec if print_sym else '' )
  206. def parse_bytespec(nbytes):
  207. import re
  208. m = re.match(r'([0123456789.]+)(.*)',nbytes)
  209. if m:
  210. if m.group(2):
  211. for k,v in bytespec_map:
  212. if k == m.group(2):
  213. from decimal import Decimal
  214. return int(Decimal(m.group(1)) * v)
  215. else:
  216. msg("Valid byte specifiers: '{}'".format("' '".join([i[0] for i in bytespec_map])))
  217. elif '.' in nbytes:
  218. raise ValueError('fractional bytes not allowed')
  219. else:
  220. return int(nbytes)
  221. die(1,f'{nbytes!r}: invalid byte specifier')
  222. def check_or_create_dir(path):
  223. try:
  224. os.listdir(path)
  225. except:
  226. if os.getenv('MMGEN_TEST_SUITE'):
  227. try: # exception handling required for MSWin/MSYS2
  228. run(['/bin/rm','-rf',path])
  229. except:
  230. pass
  231. try:
  232. os.makedirs(path,0o700)
  233. except:
  234. die(2,f'ERROR: unable to read or create path {path!r}')
  235. from .opts import opt
  236. def qmsg(s,alt=None):
  237. if opt.quiet:
  238. if alt != None: msg(alt)
  239. else: msg(s)
  240. def qmsg_r(s,alt=None):
  241. if opt.quiet:
  242. if alt != None: msg_r(alt)
  243. else: msg_r(s)
  244. def vmsg(s,force=False):
  245. if opt.verbose or force: msg(s)
  246. def vmsg_r(s,force=False):
  247. if opt.verbose or force: msg_r(s)
  248. def Vmsg(s,force=False):
  249. if opt.verbose or force: Msg(s)
  250. def Vmsg_r(s,force=False):
  251. if opt.verbose or force: Msg_r(s)
  252. def dmsg(s):
  253. if opt.debug: msg(s)
  254. def suf(arg,suf_type='s',verb='none'):
  255. suf_types = {
  256. 'none': {
  257. 's': ('s', ''),
  258. 'es': ('es', ''),
  259. 'ies': ('ies','y'),
  260. },
  261. 'is': {
  262. 's': ('s are', ' is'),
  263. 'es': ('es are', ' is'),
  264. 'ies': ('ies are','y is'),
  265. },
  266. 'has': {
  267. 's': ('s have', ' has'),
  268. 'es': ('es have', ' has'),
  269. 'ies': ('ies have','y has'),
  270. },
  271. }
  272. if isinstance(arg,int):
  273. n = arg
  274. elif isinstance(arg,(list,tuple,set,dict)):
  275. n = len(arg)
  276. else:
  277. die(2,f'{arg}: invalid parameter for suf()')
  278. return suf_types[verb][suf_type][n == 1]
  279. def get_extension(fn):
  280. return os.path.splitext(fn)[1][1:]
  281. def remove_extension(fn,ext):
  282. a,b = os.path.splitext(fn)
  283. return a if b[1:] == ext else fn
  284. def make_chksum_N(s,nchars,sep=False):
  285. if isinstance(s,str): s = s.encode()
  286. if nchars%4 or not (4 <= nchars <= 64): return False
  287. s = sha256(sha256(s).digest()).hexdigest().upper()
  288. sep = ('',' ')[bool(sep)]
  289. return sep.join([s[i*4:i*4+4] for i in range(nchars//4)])
  290. def make_chksum_8(s,sep=False):
  291. from .obj import HexStr
  292. s = HexStr(sha256(sha256(s).digest()).hexdigest()[:8].upper(),case='upper')
  293. return '{} {}'.format(s[:4],s[4:]) if sep else s
  294. def make_chksum_6(s):
  295. from .obj import HexStr
  296. if isinstance(s,str): s = s.encode()
  297. return HexStr(sha256(s).hexdigest()[:6])
  298. def is_chksum_6(s): return len(s) == 6 and is_hex_str_lc(s)
  299. def make_iv_chksum(s): return sha256(s).hexdigest()[:8].upper()
  300. def splitN(s,n,sep=None): # always return an n-element list
  301. ret = s.split(sep,n-1)
  302. return ret + ['' for i in range(n-len(ret))]
  303. def split2(s,sep=None): return splitN(s,2,sep) # always return a 2-element list
  304. def split3(s,sep=None): return splitN(s,3,sep) # always return a 3-element list
  305. def split_into_cols(col_wid,s):
  306. return ' '.join([s[col_wid*i:col_wid*(i+1)] for i in range(len(s)//col_wid+1)]).rstrip()
  307. def capfirst(s): # different from str.capitalize() - doesn't downcase any uc in string
  308. return s if len(s) == 0 else s[0].upper() + s[1:]
  309. def decode_timestamp(s):
  310. # tz_save = open('/etc/timezone').read().rstrip()
  311. os.environ['TZ'] = 'UTC'
  312. ts = time.strptime(s,'%Y%m%d_%H%M%S')
  313. t = time.mktime(ts)
  314. # os.environ['TZ'] = tz_save
  315. return int(t)
  316. def make_timestamp(secs=None):
  317. t = int(secs) if secs else time.time()
  318. return '{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}'.format(*time.gmtime(t)[:6])
  319. def make_timestr(secs=None):
  320. t = int(secs) if secs else time.time()
  321. return '{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}'.format(*time.gmtime(t)[:6])
  322. def secs_to_dhms(secs):
  323. dsecs = secs // 3600
  324. return '{}{:02d}:{:02d}:{:02d} h/m/s'.format(
  325. ('{} day{}, '.format(dsecs//24,suf(dsecs//24)) if dsecs > 24 else ''),
  326. dsecs % 24,
  327. (secs // 60) % 60,
  328. secs % 60
  329. )
  330. def secs_to_hms(secs):
  331. return '{:02d}:{:02d}:{:02d}'.format(secs//3600, (secs//60) % 60, secs % 60)
  332. def secs_to_ms(secs):
  333. return '{:02d}:{:02d}'.format(secs//60, secs % 60)
  334. def is_int(s):
  335. try:
  336. int(str(s))
  337. return True
  338. except:
  339. return False
  340. def is_hex_str(s): return set(list(s.lower())) <= set(list(hexdigits.lower()))
  341. def is_hex_str_lc(s): return set(list(s)) <= set(list(hexdigits.lower()))
  342. def is_utf8(s):
  343. try: s.decode('utf8')
  344. except: return False
  345. else: return True
  346. def remove_whitespace(s,ws='\t\r\n '):
  347. return s.translate(dict((ord(e),None) for e in ws))
  348. def pretty_format(s,width=80,pfx=''):
  349. out = []
  350. while(s):
  351. if len(s) <= width:
  352. out.append(s)
  353. break
  354. i = s[:width].rfind(' ')
  355. out.append(s[:i])
  356. s = s[i+1:]
  357. return pfx + ('\n'+pfx).join(out)
  358. def block_format(data,gw=2,cols=8,line_nums=None,data_is_hex=False):
  359. assert line_nums in (None,'hex','dec'),"'line_nums' must be one of None, 'hex' or 'dec'"
  360. ln_fs = '{:06x}: ' if line_nums == 'hex' else '{:06}: '
  361. bytes_per_chunk = gw
  362. if data_is_hex:
  363. gw *= 2
  364. nchunks = len(data)//gw + bool(len(data)%gw)
  365. return ''.join(
  366. ('' if (line_nums == None or i % cols) else ln_fs.format(i*bytes_per_chunk))
  367. + data[i*gw:i*gw+gw]
  368. + (' ' if (not cols or (i+1) % cols) else '\n')
  369. for i in range(nchunks)
  370. ).rstrip() + '\n'
  371. def pretty_hexdump(data,gw=2,cols=8,line_nums=None):
  372. return block_format(data.hex(),gw,cols,line_nums,data_is_hex=True)
  373. def decode_pretty_hexdump(data):
  374. from string import hexdigits
  375. pat = re.compile(fr'^[{hexdigits}]+:\s+')
  376. lines = [pat.sub('',line) for line in data.splitlines()]
  377. try:
  378. return bytes.fromhex(''.join((''.join(lines).split())))
  379. except:
  380. msg('Data not in hexdump format')
  381. return False
  382. def strip_comment(line):
  383. return re.sub(r'\s+$','',re.sub(r'#.*','',line))
  384. def strip_comments(lines):
  385. return [m for m in [strip_comment(l) for l in lines] if m != '']
  386. def get_hash_params(hash_preset):
  387. if hash_preset in g.hash_presets:
  388. return g.hash_presets[hash_preset] # N,r,p
  389. else: # Shouldn't be here
  390. die(3,f"{hash_preset}: invalid 'hash_preset' value")
  391. def compare_chksums(chk1,desc1,chk2,desc2,hdr='',die_on_fail=False,verbose=False):
  392. if not chk1 == chk2:
  393. fs = "{} ERROR: {} checksum ({}) doesn't match {} checksum ({})"
  394. m = fs.format((hdr+':\n ' if hdr else 'CHECKSUM'),desc2,chk2,desc1,chk1)
  395. if die_on_fail:
  396. die(3,m)
  397. else:
  398. vmsg(m,force=verbose)
  399. return False
  400. vmsg(f'{capfirst(desc1)} checksum OK ({chk1})')
  401. return True
  402. def compare_or_die(val1, desc1, val2, desc2, e='Error'):
  403. if val1 != val2:
  404. die(3,f"{e}: {desc2} ({val2}) doesn't match {desc1} ({val1})")
  405. dmsg(f'{capfirst(desc2)} OK ({val2})')
  406. return True
  407. def check_binary(args):
  408. try:
  409. run(args,stdout=DEVNULL,stderr=DEVNULL,check=True)
  410. except:
  411. rdie(2,f'{args[0]!r} binary missing, not in path, or not executable')
  412. def shred_file(fn,verbose=False):
  413. check_binary(['shred','--version'])
  414. run(
  415. ['shred','--force','--iterations=30','--zero','--remove=wipesync']
  416. + (['--verbose'] if verbose else [])
  417. + [fn],
  418. check=True )
  419. def open_file_or_exit(filename,mode,silent=False):
  420. try:
  421. return open(filename, mode)
  422. except:
  423. op = ('writing','reading')['r' in mode]
  424. die(2,'' if silent else f'Unable to open file {filename!r} for {op}')
  425. def check_file_type_and_access(fname,ftype,blkdev_ok=False):
  426. access,op_desc = (
  427. (os.W_OK,'writ') if ftype in ('output file','output directory') else
  428. (os.R_OK,'read') )
  429. if ftype == 'output directory':
  430. ok_types = [(stat.S_ISDIR, 'output directory')]
  431. else:
  432. ok_types = [
  433. (stat.S_ISREG,'regular file'),
  434. (stat.S_ISLNK,'symbolic link')
  435. ]
  436. if blkdev_ok:
  437. ok_types.append((stat.S_ISBLK,'block device'))
  438. try:
  439. mode = os.stat(fname).st_mode
  440. except:
  441. raise FileNotFound(f'Requested {ftype} {fname!r} not found')
  442. for t in ok_types:
  443. if t[0](mode):
  444. break
  445. else:
  446. ok_list = ' or '.join( t[1] for t in ok_types )
  447. die(1,f'Requested {ftype} {fname!r} is not a {ok_list}')
  448. if not os.access(fname,access):
  449. die(1,f'Requested {ftype} {fname!r} is not {op_desc}able by you')
  450. return True
  451. def check_infile(f,blkdev_ok=False):
  452. return check_file_type_and_access(f,'input file',blkdev_ok=blkdev_ok)
  453. def check_outfile(f,blkdev_ok=False):
  454. return check_file_type_and_access(f,'output file',blkdev_ok=blkdev_ok)
  455. def check_outdir(f):
  456. return check_file_type_and_access(f,'output directory')
  457. def check_wallet_extension(fn):
  458. from .wallet import Wallet
  459. if not Wallet.ext_to_type(get_extension(fn)):
  460. raise BadFileExtension(f'{fn!r}: unrecognized seed source file extension')
  461. def make_full_path(outdir,outfile):
  462. return os.path.normpath(os.path.join(outdir, os.path.basename(outfile)))
  463. def get_seed_file(cmd_args,nargs,invoked_as=None):
  464. from .filename import find_file_in_dir
  465. from .wallet import MMGenWallet
  466. wf = find_file_in_dir(MMGenWallet,g.data_dir)
  467. wd_from_opt = bool(opt.hidden_incog_input_params or opt.in_fmt) # have wallet data from opt?
  468. import mmgen.opts as opts
  469. if len(cmd_args) + (wd_from_opt or bool(wf)) < nargs:
  470. if not wf:
  471. msg('No default wallet found, and no other seed source was specified')
  472. opts.usage()
  473. elif len(cmd_args) > nargs:
  474. opts.usage()
  475. elif len(cmd_args) == nargs and wf and invoked_as != 'gen':
  476. qmsg('Warning: overriding default wallet with user-supplied wallet')
  477. if cmd_args or wf:
  478. check_infile(cmd_args[0] if cmd_args else wf)
  479. return cmd_args[0] if cmd_args else (wf,None)[wd_from_opt]
  480. def confirm_or_raise(message,q,expect='YES',exit_msg='Exiting at user request'):
  481. if message.strip():
  482. msg(message.strip())
  483. a = f'{q} ' if q[0].isupper() else f'Are you sure you want to {q}?\n'
  484. b = f'Type uppercase {expect!r} to confirm: '
  485. if my_raw_input(a+b).strip() != expect:
  486. raise UserNonConfirmation(exit_msg)
  487. def write_data_to_file( outfile,data,desc='data',
  488. ask_write=False,
  489. ask_write_prompt='',
  490. ask_write_default_yes=True,
  491. ask_overwrite=True,
  492. ask_tty=True,
  493. no_tty=False,
  494. quiet=False,
  495. binary=False,
  496. ignore_opt_outdir=False,
  497. check_data=False,
  498. cmp_data=None):
  499. if quiet: ask_tty = ask_overwrite = False
  500. if opt.quiet: ask_overwrite = False
  501. if ask_write_default_yes == False or ask_write_prompt:
  502. ask_write = True
  503. def do_stdout():
  504. qmsg('Output to STDOUT requested')
  505. if g.stdin_tty:
  506. if no_tty:
  507. die(2,f'Printing {desc} to screen is not allowed')
  508. if (ask_tty and not opt.quiet) or binary:
  509. confirm_or_raise('',f'output {desc} to screen')
  510. else:
  511. try: of = os.readlink(f'/proc/{os.getpid()}/fd/1') # Linux
  512. except: of = None # Windows
  513. if of:
  514. if of[:5] == 'pipe:':
  515. if no_tty:
  516. die(2,f'Writing {desc} to pipe is not allowed')
  517. if ask_tty and not opt.quiet:
  518. confirm_or_raise('',f'output {desc} to pipe')
  519. msg('')
  520. of2,pd = os.path.relpath(of),os.path.pardir
  521. msg('Redirecting output to file {!r}'.format(of if of2[:len(pd)] == pd else of2))
  522. else:
  523. msg('Redirecting output to file')
  524. if binary and g.platform == 'win':
  525. import msvcrt
  526. msvcrt.setmode(sys.stdout.fileno(),os.O_BINARY)
  527. # MSWin workaround. See msg_r()
  528. try:
  529. sys.stdout.write(data.decode() if isinstance(data,bytes) else data)
  530. except:
  531. os.write(1,data if isinstance(data,bytes) else data.encode())
  532. def do_file(outfile,ask_write_prompt):
  533. if opt.outdir and not ignore_opt_outdir and not os.path.isabs(outfile):
  534. outfile = make_full_path(opt.outdir,outfile)
  535. if ask_write:
  536. if not ask_write_prompt:
  537. ask_write_prompt = f'Save {desc}?'
  538. if not keypress_confirm(ask_write_prompt,
  539. default_yes=ask_write_default_yes):
  540. die(1,f'{capfirst(desc)} not saved')
  541. hush = False
  542. if os.path.lexists(outfile) and ask_overwrite:
  543. confirm_or_raise('',f'File {outfile!r} already exists\nOverwrite?')
  544. msg(f'Overwriting file {outfile!r}')
  545. hush = True
  546. # not atomic, but better than nothing
  547. # if cmp_data is empty, file can be either empty or non-existent
  548. if check_data:
  549. try:
  550. d = open(outfile,('r','rb')[bool(binary)]).read()
  551. except:
  552. d = ''
  553. finally:
  554. if d != cmp_data:
  555. if g.test_suite:
  556. print_diff(cmp_data,d)
  557. die(3,f'{desc} in file {outfile!r} has been altered by some other program! Aborting file write')
  558. # To maintain portability, always open files in binary mode
  559. # If 'binary' option not set, encode/decode data before writing and after reading
  560. f = open_file_or_exit(outfile,'wb')
  561. try:
  562. f.write(data if binary else data.encode())
  563. except:
  564. die(2,f'Failed to write {desc} to file {outfile!r}')
  565. f.close
  566. if not (hush or quiet):
  567. msg(f'{capfirst(desc)} written to file {outfile!r}')
  568. return True
  569. if opt.stdout or outfile in ('','-'):
  570. do_stdout()
  571. elif sys.stdin.isatty() and not sys.stdout.isatty():
  572. do_stdout()
  573. else:
  574. do_file(outfile,ask_write_prompt)
  575. def get_words_from_user(prompt):
  576. words = my_raw_input(prompt, echo=opt.echo_passphrase).split()
  577. dmsg('Sanitized input: [{}]'.format(' '.join(words)))
  578. return words
  579. def get_words_from_file(infile,desc,quiet=False):
  580. if not quiet:
  581. qmsg(f'Getting {desc} from file {infile!r}')
  582. f = open_file_or_exit(infile, 'rb')
  583. try: words = f.read().decode().split()
  584. except: die(1,f'{capfirst(desc)} data must be UTF-8 encoded.')
  585. f.close()
  586. dmsg('Sanitized input: [{}]'.format(' '.join(words)))
  587. return words
  588. def get_words(infile,desc,prompt):
  589. if infile:
  590. return get_words_from_file(infile,desc)
  591. else:
  592. return get_words_from_user(prompt)
  593. def mmgen_decrypt_file_maybe(fn,desc='',quiet=False,silent=False):
  594. d = get_data_from_file(fn,desc,binary=True,quiet=quiet,silent=silent)
  595. have_enc_ext = get_extension(fn) == g.mmenc_ext
  596. if have_enc_ext or not is_utf8(d):
  597. m = ('Attempting to decrypt','Decrypting')[have_enc_ext]
  598. qmsg(f'{m} {desc} {fn!r}')
  599. from .crypto import mmgen_decrypt_retry
  600. d = mmgen_decrypt_retry(d,desc)
  601. return d
  602. def get_lines_from_file(fn,desc='',trim_comments=False,quiet=False,silent=False):
  603. dec = mmgen_decrypt_file_maybe(fn,desc,quiet=quiet,silent=silent)
  604. ret = dec.decode().splitlines()
  605. if trim_comments:
  606. ret = strip_comments(ret)
  607. dmsg(f'Got {len(ret)} lines from file {fn!r}')
  608. return ret
  609. def get_data_from_user(desc='data'): # user input MUST be UTF-8
  610. data = my_raw_input(f'Enter {desc}: ',echo=opt.echo_passphrase)
  611. dmsg(f'User input: [{data}]')
  612. return data
  613. def get_data_from_file(infile,desc='data',dash=False,silent=False,binary=False,quiet=False):
  614. if not opt.quiet and not silent and not quiet and desc:
  615. qmsg(f'Getting {desc} from file {infile!r}')
  616. if dash and infile == '-':
  617. data = os.fdopen(0,'rb').read(g.max_input_size+1)
  618. else:
  619. data = open_file_or_exit(infile,'rb',silent=silent).read(g.max_input_size+1)
  620. if not binary:
  621. data = data.decode()
  622. if len(data) == g.max_input_size + 1:
  623. raise MaxInputSizeExceeded(f'Too much input data! Max input data size: {f.max_input_size} bytes')
  624. return data
  625. class oneshot_warning:
  626. color = 'nocolor'
  627. def __init__(self,div=None,fmt_args=[],reverse=False):
  628. self.do(type(self),div,fmt_args,reverse)
  629. def do(self,wcls,div,fmt_args,reverse):
  630. def do_warning():
  631. message = getattr(wcls,'message')
  632. color = globals()[getattr(wcls,'color')]
  633. msg(color('WARNING: ' + message.format(*fmt_args)))
  634. if not hasattr(wcls,'data'):
  635. setattr(wcls,'data',[])
  636. data = getattr(wcls,'data')
  637. condition = (div in data) if reverse else (not div in data)
  638. if not div in data:
  639. data.append(div)
  640. if condition:
  641. do_warning()
  642. self.warning_shown = True
  643. else:
  644. self.warning_shown = False
  645. class oneshot_warning_group(oneshot_warning):
  646. def __init__(self,wcls,div=None,fmt_args=[],reverse=False):
  647. self.do(getattr(self,wcls),div,fmt_args,reverse)
  648. class pwfile_reuse_warning(oneshot_warning):
  649. message = 'Reusing passphrase from file {!r} at user request'
  650. def __init__(self,fn):
  651. oneshot_warning.__init__(self,div=fn,fmt_args=[fn],reverse=True)
  652. def my_raw_input(prompt,echo=True,insert_txt='',use_readline=True):
  653. try: import readline
  654. except: use_readline = False # Windows
  655. if use_readline and sys.stdout.isatty():
  656. def st_hook(): readline.insert_text(insert_txt)
  657. readline.set_startup_hook(st_hook)
  658. else:
  659. msg_r(prompt)
  660. prompt = ''
  661. from .term import kb_hold_protect
  662. kb_hold_protect()
  663. if g.test_suite_popen_spawn:
  664. msg(prompt)
  665. sys.stderr.flush()
  666. reply = os.read(0,4096).decode()
  667. elif echo or not sys.stdin.isatty():
  668. reply = input(prompt)
  669. else:
  670. from getpass import getpass
  671. if g.platform == 'win':
  672. # MSWin hack - getpass('foo') doesn't flush stderr
  673. msg_r(prompt.strip()) # getpass('') adds a space
  674. sys.stderr.flush()
  675. reply = getpass('')
  676. else:
  677. reply = getpass(prompt)
  678. kb_hold_protect()
  679. return reply.strip()
  680. def keypress_confirm(prompt,default_yes=False,verbose=False,no_nl=False,complete_prompt=False):
  681. if not complete_prompt:
  682. prompt = '{} {}: '.format( prompt, '(Y/n)' if default_yes else '(y/N)' )
  683. nl = f'\r{" "*len(prompt)}\r' if no_nl else '\n'
  684. if g.accept_defaults:
  685. msg(prompt)
  686. return default_yes
  687. from .term import get_char
  688. while True:
  689. reply = get_char(prompt,immed_chars='yYnN').strip('\n\r')
  690. if not reply:
  691. msg_r(nl)
  692. return True if default_yes else False
  693. elif reply in 'yYnN':
  694. msg_r(nl)
  695. return True if reply in 'yY' else False
  696. else:
  697. msg_r('\nInvalid reply\n' if verbose else '\r')
  698. def do_pager(text):
  699. pagers = ['less','more']
  700. end_msg = '\n(end of text)\n\n'
  701. # --- Non-MSYS Windows code deleted ---
  702. # raw, chop, horiz scroll 8 chars, disable buggy line chopping in MSYS
  703. os.environ['LESS'] = (('--shift 8 -RS'),('-cR -#1'))[g.platform=='win']
  704. if 'PAGER' in os.environ and os.environ['PAGER'] != pagers[0]:
  705. pagers = [os.environ['PAGER']] + pagers
  706. for pager in pagers:
  707. try:
  708. m = text + ('' if pager == 'less' else end_msg)
  709. p = run([pager],input=m.encode(),check=True)
  710. msg_r('\r')
  711. except:
  712. pass
  713. else:
  714. break
  715. else:
  716. Msg(text+end_msg)
  717. def do_license_msg(immed=False):
  718. if opt.quiet or g.no_license or opt.yes or not g.stdin_tty:
  719. return
  720. import mmgen.license as gpl
  721. msg(gpl.warning)
  722. from .term import get_char
  723. prompt = "Press 'w' for conditions and warranty info, or 'c' to continue: "
  724. while True:
  725. reply = get_char(prompt, immed_chars=('','wc')[bool(immed)])
  726. if reply == 'w':
  727. do_pager(gpl.conditions)
  728. elif reply == 'c':
  729. msg('')
  730. break
  731. else:
  732. msg_r('\r')
  733. msg('')
  734. def format_par(s,indent=0,width=80,as_list=False):
  735. words,lines = s.split(),[]
  736. assert width >= indent + 4,'width must be >= indent + 4'
  737. while words:
  738. line = ''
  739. while len(line) <= (width-indent) and words:
  740. if line and len(line) + len(words[0]) + 1 > width-indent: break
  741. line += ('',' ')[bool(line)] + words.pop(0)
  742. lines.append(' '*indent + line)
  743. return lines if as_list else '\n'.join(lines) + '\n'
  744. def get_subclasses(cls,names=False):
  745. def gen(cls):
  746. for i in cls.__subclasses__():
  747. yield i
  748. for j in gen(i):
  749. yield j
  750. return tuple((c.__name__ for c in gen(cls)) if names else gen(cls))
  751. def altcoin_subclass(cls,proto,mod_dir):
  752. """
  753. magic module loading and class retrieval
  754. """
  755. from .protocol import CoinProtocol
  756. if isinstance(proto,CoinProtocol.Bitcoin):
  757. return cls
  758. modname = f'mmgen.altcoins.{proto.base_coin.lower()}.{mod_dir}'
  759. import importlib
  760. if mod_dir == 'tx': # nested classes
  761. outer_clsname,inner_clsname = (
  762. proto.mod_clsname
  763. + ('Token' if proto.tokensym else '')
  764. + cls.__qualname__ ).split('.')
  765. return getattr(getattr(importlib.import_module(modname),outer_clsname),inner_clsname)
  766. else:
  767. clsname = (
  768. proto.mod_clsname
  769. + ('Token' if proto.tokensym else '')
  770. + cls.__name__ )
  771. return getattr(importlib.import_module(modname),clsname)
  772. # decorator for TrackingWallet
  773. def write_mode(orig_func):
  774. def f(self,*args,**kwargs):
  775. if self.mode != 'w':
  776. die(1,'{} opened in read-only mode: cannot execute method {}()'.format(
  777. type(self).__name__,
  778. locals()['orig_func'].__name__
  779. ))
  780. return orig_func(self,*args,**kwargs)
  781. return f
  782. def run_session(callback,backend=None):
  783. backend = backend or opt.rpc_backend
  784. import asyncio
  785. async def do():
  786. if backend == 'aiohttp':
  787. import aiohttp
  788. async with aiohttp.ClientSession(
  789. headers = { 'Content-Type': 'application/json' },
  790. connector = aiohttp.TCPConnector(limit_per_host=g.aiohttp_rpc_queue_len),
  791. ) as g.session:
  792. ret = await callback
  793. return ret
  794. else:
  795. return await callback
  796. return asyncio.run(do())