fa6125de4455fb187db8888e510400e9e9820b3b
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

1) # SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info>
2) #
3) # SPDX-License-Identifier: MIT
4) 
5) """Command-line interface for derivepassphrase_export."""
6) 
7) from __future__ import annotations
8) 
9) import base64
10) import importlib
11) import json
12) import logging
13) from typing import TYPE_CHECKING, Any, Literal
14) 
15) import click
16) from typing_extensions import assert_never
17) 
18) import derivepassphrase as dpp
19) from derivepassphrase import _types, exporter
20) 
21) if TYPE_CHECKING:
22)     import os
23)     import types
24)     from collections.abc import Sequence
25) 
26) __author__ = dpp.__author__
27) __version__ = dpp.__version__
28) 
29) __all__ = ('derivepassphrase_export',)
30) 
31) PROG_NAME = 'derivepassphrase_export'
32) 
33) 
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

34) def _load_data(
35)     fmt: Literal['v0.2', 'v0.3', 'storeroom'],
36)     path: str | bytes | os.PathLike[str],
37)     key: bytes,
38) ) -> Any:
39)     contents: bytes
40)     module: types.ModuleType
41)     match fmt:
42)         case 'v0.2':
43)             module = importlib.import_module(
44)                 'derivepassphrase.exporter.vault_v03_and_below'
45)             )
46)             if module.STUBBED:
47)                 raise ModuleNotFoundError
48)             with open(path, 'rb') as infile:
49)                 contents = base64.standard_b64decode(infile.read())
50)             return module.V02Reader(contents, key).run()
51)         case 'v0.3':
52)             module = importlib.import_module(
53)                 'derivepassphrase.exporter.vault_v03_and_below'
54)             )
55)             if module.STUBBED:
56)                 raise ModuleNotFoundError
57)             with open(path, 'rb') as infile:
58)                 contents = base64.standard_b64decode(infile.read())
59)             return module.V03Reader(contents, key).run()
60)         case 'storeroom':
61)             module = importlib.import_module(
62)                 'derivepassphrase.exporter.storeroom'
63)             )
64)             if module.STUBBED:
65)                 raise ModuleNotFoundError
66)             return module.export_storeroom_data(path, key)
67)         case _:  # pragma: no cover
68)             assert_never(fmt)
69) 
70) 
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

71) @click.command(
72)     context_settings={'help_option_names': ['-h', '--help']},
73) )
74) @click.option(
75)     '-f',
76)     '--format',
77)     'formats',
78)     metavar='FMT',
79)     multiple=True,
80)     default=('v0.3', 'v0.2', 'storeroom'),
81)     type=click.Choice(['v0.2', 'v0.3', 'storeroom']),
82)     help='try the following storage formats, in order (default: v0.3, v0.2)',
83) )
84) @click.option(
85)     '-k',
86)     '--key',
87)     metavar='K',
88)     help=(
89)         'use K as the storage master key '
90)         '(default: check the `VAULT_KEY`, `LOGNAME`, `USER` or '
91)         '`USERNAME` environment variables)'
92)     ),
93) )
94) @click.argument('path', metavar='PATH', required=True)
95) @click.pass_context
96) def derivepassphrase_export(
97)     ctx: click.Context,
98)     /,
99)     *,
100)     path: str | bytes | os.PathLike[str],
101)     formats: Sequence[Literal['v0.2', 'v0.3', 'storeroom']] = (),
102)     key: str | bytes | None = None,
103) ) -> None:
104)     """Export a vault-native configuration to standard output.
105) 
106)     Read the vault-native configuration at PATH, extract all information
107)     from it, and export the resulting configuration to standard output.
108)     Depending on the configuration format, this may either be a file or
109)     a directory.
110) 
111)     If PATH is explicitly given as `VAULT_PATH`, then use the
112)     `VAULT_PATH` environment variable to determine the correct path.
113)     (Use `./VAULT_PATH` or similar to indicate a file/directory actually
114)     named `VAULT_PATH`.)
115) 
116)     """
117) 
118)     logging.basicConfig()
119)     if path in {'VAULT_PATH', b'VAULT_PATH'}:
120)         path = exporter.get_vault_path()
121)     if key is None:
122)         key = exporter.get_vault_key()
123)     elif isinstance(key, str):  # pragma: no branch
124)         key = key.encode('utf-8')
125)     for fmt in formats:
126)         try:
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

127)             config = _load_data(fmt, path, key)
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

128)         except (
129)             IsADirectoryError,
130)             NotADirectoryError,
131)             ValueError,
132)             RuntimeError,
133)         ):
134)             logging.info('Cannot load as %s: %s', fmt, path)
135)             continue
136)         except OSError as exc:
137)             click.echo(
138)                 (
139)                     f'{PROG_NAME}: ERROR: Cannot parse {path!r} as '
140)                     f'a valid config: {exc.strerror}: {exc.filename!r}'
141)                 ),
142)                 err=True,
143)             )
144)             ctx.exit(1)
145)         except ModuleNotFoundError:
146)             # TODO(the-13th-letter): Use backslash continuation.
147)             # https://github.com/nedbat/coveragepy/issues/1836
148)             msg = f"""
149) {PROG_NAME}: ERROR: Cannot load the required Python module "cryptography".
150) {PROG_NAME}: INFO: pip users: see the "export" extra.
151) """.lstrip('\n')
152)             click.echo(msg, nl=False, err=True)
153)             ctx.exit(1)
154)         else:
155)             if not _types.is_vault_config(config):
156)                 click.echo(
157)                     f'{PROG_NAME}: ERROR: Invalid vault config: {config!r}',
158)                     err=True,
159)                 )
160)                 ctx.exit(1)
161)             click.echo(json.dumps(config, indent=2, sort_keys=True))
162)             break
163)     else:
164)         click.echo(
165)             f'{PROG_NAME}: ERROR: Cannot parse {path!r} as a valid config.',
166)             err=True,
167)         )
168)         ctx.exit(1)