e8f3ec854c425cc36565a40adbf00d22a2febeec
Marco Ricci Change the author e-mail ad...

Marco Ricci authored 1 week ago

1) # SPDX-FileCopyrightText: 2024 Marco Ricci <software@the13thletter.info>
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

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,
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

39)     contents: bytes
40)     module: types.ModuleType
41)     match fmt:
42)         case 'v0.2':
43)             module = importlib.import_module(
Marco Ricci Rename `vault_v03_and_below...

Marco Ricci authored 2 weeks ago

44)                 'derivepassphrase.exporter.vault_native'
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

45)             )
46)             if module.STUBBED:
47)                 raise ModuleNotFoundError
48)             with open(path, 'rb') as infile:
49)                 contents = base64.standard_b64decode(infile.read())
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 2 weeks ago

50)             return module.export_vault_native_data(
51)                 contents, key, try_formats=['v0.2']
52)             )
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

53)         case 'v0.3':
54)             module = importlib.import_module(
Marco Ricci Rename `vault_v03_and_below...

Marco Ricci authored 2 weeks ago

55)                 'derivepassphrase.exporter.vault_native'
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

56)             )
57)             if module.STUBBED:
58)                 raise ModuleNotFoundError
59)             with open(path, 'rb') as infile:
60)                 contents = base64.standard_b64decode(infile.read())
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 2 weeks ago

61)             return module.export_vault_native_data(
62)                 contents, key, try_formats=['v0.3']
63)             )
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

64)         case 'storeroom':
65)             module = importlib.import_module(
66)                 'derivepassphrase.exporter.storeroom'
67)             )
68)             if module.STUBBED:
69)                 raise ModuleNotFoundError
70)             return module.export_storeroom_data(path, key)
71)         case _:  # pragma: no cover
72)             assert_never(fmt)
73) 
74) 
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

75) @click.command(
76)     context_settings={'help_option_names': ['-h', '--help']},
77) )
78) @click.option(
79)     '-f',
80)     '--format',
81)     'formats',
82)     metavar='FMT',
83)     multiple=True,
84)     default=('v0.3', 'v0.2', 'storeroom'),
85)     type=click.Choice(['v0.2', 'v0.3', 'storeroom']),
86)     help='try the following storage formats, in order (default: v0.3, v0.2)',
87) )
88) @click.option(
89)     '-k',
90)     '--key',
91)     metavar='K',
92)     help=(
93)         'use K as the storage master key '
94)         '(default: check the `VAULT_KEY`, `LOGNAME`, `USER` or '
95)         '`USERNAME` environment variables)'
96)     ),
97) )
98) @click.argument('path', metavar='PATH', required=True)
99) @click.pass_context
100) def derivepassphrase_export(
101)     ctx: click.Context,
102)     /,
103)     *,
104)     path: str | bytes | os.PathLike[str],
105)     formats: Sequence[Literal['v0.2', 'v0.3', 'storeroom']] = (),
106)     key: str | bytes | None = None,
107) ) -> None:
108)     """Export a vault-native configuration to standard output.
109) 
110)     Read the vault-native configuration at PATH, extract all information
111)     from it, and export the resulting configuration to standard output.
112)     Depending on the configuration format, this may either be a file or
Marco Ricci Add man page for `derivepas...

Marco Ricci authored 2 weeks ago

113)     a directory.  Supports the vault "v0.2", "v0.3" and "storeroom"
114)     formats.
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

115) 
116)     If PATH is explicitly given as `VAULT_PATH`, then use the
117)     `VAULT_PATH` environment variable to determine the correct path.
118)     (Use `./VAULT_PATH` or similar to indicate a file/directory actually
119)     named `VAULT_PATH`.)
120) 
121)     """
122)     logging.basicConfig()
123)     if path in {'VAULT_PATH', b'VAULT_PATH'}:
124)         path = exporter.get_vault_path()
125)     if key is None:
126)         key = exporter.get_vault_key()
127)     elif isinstance(key, str):  # pragma: no branch
128)         key = key.encode('utf-8')
129)     for fmt in formats:
130)         try:
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

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