1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
|
# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
# SPDX-License-Identifier: GPL-2.0-or-later
"""Parse the output of `gpg --list-keys --with-colons`."""
from __future__ import annotations
import dataclasses
import typing
import pyparsing as pyp
from testsigs import defs
if typing.TYPE_CHECKING:
from typing import Final
_p_eol = pyp.Char("\r\n").suppress()
_p_ign = pyp.ZeroOrMore(pyp.CharsNotIn(":\n")).suppress() + pyp.Char(":").suppress()
_p_ign_all = pyp.CharsNotIn("\n").suppress()
_p_hex_string = pyp.Word("0123456789ABCDEF")
_p_string = pyp.CharsNotIn(":\n")
_p_ignored_line = pyp.Literal("tru:").suppress() + _p_ign_all + _p_eol
_p_ignored_lines = pyp.ZeroOrMore(_p_ignored_line)
_p_list_pub = (
pyp.Literal("pub:").suppress() + _p_ign + _p_ign + _p_ign + _p_hex_string + _p_ign_all + _p_eol
)
_p_list_fpr = pyp.Literal("fpr:").suppress() + _p_ign * 8 + _p_hex_string + _p_ign_all + _p_eol
_p_list_uid = pyp.Literal("uid:").suppress() + _p_ign * 8 + _p_string + _p_ign_all + _p_eol
_p_list_sub = (
pyp.Literal("sub:").suppress()
+ _p_ign
+ _p_ign
+ _p_ign
+ _p_hex_string
+ _p_ign * 7
+ _p_string
+ _p_ign_all
+ _p_eol
)
@_p_list_sub.set_parse_action
def _parse_list_sub(tokens: pyp.ParseResults) -> defs.Subkey:
"""Parse a subkey definition."""
params: Final = tokens.as_list()
match params:
case [key_id, caps]:
return defs.Subkey(key_id=key_id, fpr="", capabilities=set(caps))
case _:
raise RuntimeError(repr(params))
_p_list_subkey = _p_ignored_lines + _p_list_sub + _p_ignored_lines + _p_list_fpr
@_p_list_subkey.set_parse_action
def _parse_list_subkey(tokens: pyp.ParseResults) -> defs.Subkey:
"""Parse a subkey definition."""
params: Final = tokens.as_list()
match params:
case [sub_key_id, fpr]:
return dataclasses.replace(sub_key_id, fpr=fpr)
case _:
raise RuntimeError(repr(params))
_p_list_pubkey = (
_p_ignored_lines
+ _p_list_pub
+ _p_ignored_lines
+ _p_list_fpr
+ _p_ignored_lines
+ _p_list_uid
+ pyp.OneOrMore(_p_list_subkey)
)
@_p_list_pubkey.set_parse_action
def _parse_list_pubkey(tokens: pyp.ParseResults) -> defs.PublicKey:
"""Parse a subkey definition."""
params: Final = tokens.as_list()
match params:
case [key_id, fpr, uid, *subkeys] if isinstance(key_id, str) and isinstance(
fpr,
str,
) and isinstance(uid, str) and subkeys and all(
isinstance(subkey, defs.Subkey) for subkey in subkeys
):
# We did just validate that one...
return defs.PublicKey(
key_id=key_id,
fpr=fpr,
uid=uid,
subkeys=subkeys, # type: ignore[arg-type]
)
case _:
raise RuntimeError(repr(params))
_p_list_keys = pyp.ZeroOrMore(_p_list_pubkey) + _p_ignored_lines
_p_list_keys_complete = _p_list_keys.leave_whitespace().parse_with_tabs()
def parse_list_keys(output: str) -> list[defs.PublicKey]:
"""Parse the output of `gpg --list-keys`, extract the info we care about."""
res: Final = _p_list_keys_complete.parse_string(output, parse_all=True).as_list()
if not all(isinstance(pkey, defs.PublicKey) for pkey in res):
raise RuntimeError(repr(res))
return res
|