util.py 25 KB

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