2d292af3e81527750e46a2167d30efe840ac58ca
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 months 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 months ago

34) def _load_data(
35)     fmt: Literal['v0.2', 'v0.3', 'storeroom'],
36)     path: str | bytes | os.PathLike[str],
37)     key: bytes,
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

38) ) -> Any:  # noqa: ANN401
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 months ago

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())
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 months ago

50)             return module.VaultNativeV02ConfigParser(contents, key)()
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 months ago

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())
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 months ago

59)             return module.VaultNativeV03ConfigParser(contents, key)()
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 months ago

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 months 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)     logging.basicConfig()
118)     if path in {'VAULT_PATH', b'VAULT_PATH'}:
119)         path = exporter.get_vault_path()
120)     if key is None:
121)         key = exporter.get_vault_key()
122)     elif isinstance(key, str):  # pragma: no branch
123)         key = key.encode('utf-8')
124)     for fmt in formats:
125)         try:
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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