From: marcandre.lureau@redhat.com
To: qemu-devel@nongnu.org
Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>,
jsnow@redhat.com, armbru@redhat.com
Subject: [PATCH v3 6/9] qapi: normalize 'if' condition to IfPredicate tree
Date: Thu, 29 Apr 2021 17:40:29 +0400 [thread overview]
Message-ID: <20210429134032.1125111-7-marcandre.lureau@redhat.com> (raw)
In-Reply-To: <20210429134032.1125111-1-marcandre.lureau@redhat.com>
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Modify check_if() to build an IfPredicate tree (the schema
documentation is updated in a following patch).
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
tests/unit/test-qmp-cmds.c | 1 +
scripts/qapi/expr.py | 62 ++++++++++++++-----
scripts/qapi/schema.py | 13 +---
tests/qapi-schema/bad-if.err | 3 +-
tests/qapi-schema/doc-good.out | 12 ++--
tests/qapi-schema/enum-if-invalid.err | 3 +-
tests/qapi-schema/features-if-invalid.err | 2 +-
tests/qapi-schema/qapi-schema-test.json | 20 +++---
tests/qapi-schema/qapi-schema-test.out | 59 ++++++++++--------
.../qapi-schema/struct-member-if-invalid.err | 2 +-
10 files changed, 106 insertions(+), 71 deletions(-)
diff --git a/tests/unit/test-qmp-cmds.c b/tests/unit/test-qmp-cmds.c
index 1b0b7d99df..83efa39720 100644
--- a/tests/unit/test-qmp-cmds.c
+++ b/tests/unit/test-qmp-cmds.c
@@ -51,6 +51,7 @@ FeatureStruct1 *qmp_test_features0(bool has_fs0, FeatureStruct0 *fs0,
bool has_cfs1, CondFeatureStruct1 *cfs1,
bool has_cfs2, CondFeatureStruct2 *cfs2,
bool has_cfs3, CondFeatureStruct3 *cfs3,
+ bool has_cfs4, CondFeatureStruct4 *cfs4,
Error **errp)
{
return g_new0(FeatureStruct1, 1);
diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index 496f7e0333..0a97a6f020 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -42,7 +42,14 @@
cast,
)
-from .common import c_name
+from .common import (
+ IfAll,
+ IfAny,
+ IfNot,
+ IfOption,
+ IfPredicate,
+ c_name,
+)
from .error import QAPISemError
from .parser import QAPIDoc
from .source import QAPISourceInfo
@@ -261,6 +268,10 @@ def check_if(expr: _JSONObject, info: QAPISourceInfo, source: str) -> None:
"""
Normalize and validate the ``if`` member of an object.
+ The ``if`` field may be either a ``str``, a ``List[str]`` or a dict.
+ A ``str`` element or a ``List[str]`` will be normalized to
+ ``IfAll([str])``.
+
The ``if`` member may be either a ``str`` or a ``List[str]``.
A ``str`` value will be normalized to ``List[str]``.
@@ -281,25 +292,44 @@ def check_if(expr: _JSONObject, info: QAPISourceInfo, source: str) -> None:
if ifcond is None:
return
- if isinstance(ifcond, list):
- if not ifcond:
- raise QAPISemError(
- info, "'if' condition [] of %s is useless" % source)
- else:
- # Normalize to a list
- ifcond = expr['if'] = [ifcond]
-
- for elt in ifcond:
- if not isinstance(elt, str):
+ def normalize(cond: Union[str, List[str], object]) -> IfPredicate:
+ if isinstance(cond, str):
+ if not cond.strip():
+ raise QAPISemError(
+ info,
+ "'if' condition '%s' of %s makes no sense"
+ % (cond, source))
+ return IfOption(cond)
+ if isinstance(cond, list):
+ cond = {"all": cond}
+ if not isinstance(cond, dict):
raise QAPISemError(
info,
- "'if' condition of %s must be a string or a list of strings"
- % source)
- if not elt.strip():
+ "'if' condition of %s must be a string, "
+ "a list of strings or a dict" % source)
+ if len(cond) != 1:
raise QAPISemError(
info,
- "'if' condition '%s' of %s makes no sense"
- % (elt, source))
+ "'if' condition dict of %s must have one key: "
+ "'all', 'any' or 'not'" % source)
+ check_keys(cond, info, "'if' condition", [],
+ ["all", "any", "not"])
+ oper, operands = next(iter(cond.items()))
+ if oper == "not":
+ return IfNot(normalize(operands))
+ if not operands:
+ raise QAPISemError(
+ info, "'if' condition [] of %s is useless" % source)
+ if not isinstance(operands, list):
+ raise QAPISemError(
+ info, "'%s' condition of %s must be a list" % (oper, source))
+ operands = [normalize(o) for o in operands]
+ return IfAll(operands) if oper == "all" else IfAny(operands)
+
+ ifcond = expr.get('if')
+ if ifcond is None:
+ return
+ expr['if'] = normalize(ifcond)
def normalize_members(members: object) -> None:
diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 366a53ab64..61664a4c5e 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -19,22 +19,15 @@
import re
from typing import Optional
-from .common import (
- POINTER_SUFFIX,
- IfAll,
- IfOption,
- c_name,
- mcgen,
-)
+from .common import POINTER_SUFFIX, c_name, mcgen
from .error import QAPISemError, QAPISourceError
from .expr import check_exprs
from .parser import QAPISchemaParser
class QAPISchemaIfCond:
- def __init__(self, ifcond=None):
- pred_list = [IfOption(opt) for opt in ifcond or []]
- self.pred = IfAll(pred_list)
+ def __init__(self, pred=None):
+ self.pred = pred
def gen_doc(self):
if self.pred:
diff --git a/tests/qapi-schema/bad-if.err b/tests/qapi-schema/bad-if.err
index f83dee65da..454fbae387 100644
--- a/tests/qapi-schema/bad-if.err
+++ b/tests/qapi-schema/bad-if.err
@@ -1,2 +1,3 @@
bad-if.json: In struct 'TestIfStruct':
-bad-if.json:2: 'if' condition of struct must be a string or a list of strings
+bad-if.json:2: 'if' condition has unknown key 'value'
+Valid keys are 'all', 'any', 'not'.
diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
index 6bf996f539..ca7e53f3b5 100644
--- a/tests/qapi-schema/doc-good.out
+++ b/tests/qapi-schema/doc-good.out
@@ -12,15 +12,15 @@ enum QType
module doc-good.json
enum Enum
member one
- if IfAll(['defined(IFONE)'])
+ if 'defined(IFONE)'
member two
- if IfAll(['defined(IFCOND)'])
+ if 'defined(IFCOND)'
feature enum-feat
object Base
member base1: Enum optional=False
object Variant1
member var1: str optional=False
- if IfAll(['defined(IFSTR)'])
+ if 'defined(IFSTR)'
feature member-feat
feature variant1-feat
object Variant2
@@ -29,7 +29,7 @@ object Object
tag base1
case one: Variant1
case two: Variant2
- if IfAll(['IFTWO'])
+ if 'IFTWO'
feature union-feat1
object q_obj_Variant1-wrapper
member data: Variant1 optional=False
@@ -38,13 +38,13 @@ object q_obj_Variant2-wrapper
enum SugaredUnionKind
member one
member two
- if IfAll(['IFTWO'])
+ if 'IFTWO'
object SugaredUnion
member type: SugaredUnionKind optional=False
tag type
case one: q_obj_Variant1-wrapper
case two: q_obj_Variant2-wrapper
- if IfAll(['IFTWO'])
+ if 'IFTWO'
feature union-feat2
alternate Alternate
tag type
diff --git a/tests/qapi-schema/enum-if-invalid.err b/tests/qapi-schema/enum-if-invalid.err
index 0556dc967b..3bb84075a9 100644
--- a/tests/qapi-schema/enum-if-invalid.err
+++ b/tests/qapi-schema/enum-if-invalid.err
@@ -1,2 +1,3 @@
enum-if-invalid.json: In enum 'TestIfEnum':
-enum-if-invalid.json:2: 'if' condition of 'data' member 'bar' must be a string or a list of strings
+enum-if-invalid.json:2: 'if' condition has unknown key 'val'
+Valid keys are 'all', 'any', 'not'.
diff --git a/tests/qapi-schema/features-if-invalid.err b/tests/qapi-schema/features-if-invalid.err
index f63b89535e..724a810086 100644
--- a/tests/qapi-schema/features-if-invalid.err
+++ b/tests/qapi-schema/features-if-invalid.err
@@ -1,2 +1,2 @@
features-if-invalid.json: In struct 'Stru':
-features-if-invalid.json:2: 'if' condition of 'features' member 'f' must be a string or a list of strings
+features-if-invalid.json:2: 'if' condition of 'features' member 'f' must be a string, a list of strings or a dict
diff --git a/tests/qapi-schema/qapi-schema-test.json b/tests/qapi-schema/qapi-schema-test.json
index 84b9d41f15..2d5e480b44 100644
--- a/tests/qapi-schema/qapi-schema-test.json
+++ b/tests/qapi-schema/qapi-schema-test.json
@@ -231,8 +231,8 @@
{ 'union': 'TestIfUnion', 'data':
{ 'foo': 'TestStruct',
- 'bar': { 'type': 'str', 'if': 'defined(TEST_IF_UNION_BAR)'} },
- 'if': 'defined(TEST_IF_UNION) && defined(TEST_IF_STRUCT)' }
+ 'union-bar': { 'type': 'str', 'if': 'defined(TEST_IF_UNION_BAR)'} },
+ 'if': ['defined(TEST_IF_UNION)', 'defined(TEST_IF_STRUCT)'] }
{ 'command': 'test-if-union-cmd',
'data': { 'union-cmd-arg': 'TestIfUnion' },
@@ -241,11 +241,10 @@
{ 'alternate': 'TestIfAlternate', 'data':
{ 'foo': 'int',
'bar': { 'type': 'TestStruct', 'if': 'defined(TEST_IF_ALT_BAR)'} },
- 'if': 'defined(TEST_IF_ALT) && defined(TEST_IF_STRUCT)' }
+ 'if': {'all': ['defined(TEST_IF_ALT)', 'defined(TEST_IF_STRUCT)'] } }
-{ 'command': 'test-if-alternate-cmd',
- 'data': { 'alt-cmd-arg': 'TestIfAlternate' },
- 'if': 'defined(TEST_IF_ALT)' }
+{ 'command': 'test-if-alternate-cmd', 'data': { 'alt-cmd-arg': 'TestIfAlternate' },
+ 'if': {'all': ['defined(TEST_IF_ALT)', {'not': 'defined(TEST_IF_NOT_ALT)'}] } }
{ 'command': 'test-if-cmd',
'data': {
@@ -259,7 +258,7 @@
{ 'event': 'TEST_IF_EVENT', 'data':
{ 'foo': 'TestIfStruct',
'bar': { 'type': ['TestIfEnum'], 'if': 'defined(TEST_IF_EVT_BAR)' } },
- 'if': 'defined(TEST_IF_EVT) && defined(TEST_IF_STRUCT)' }
+ 'if': ['defined(TEST_IF_EVT)', 'defined(TEST_IF_STRUCT)'] }
# test 'features'
@@ -290,6 +289,10 @@
'data': { 'foo': 'int' },
'features': [ { 'name': 'feature1', 'if': [ 'defined(TEST_IF_COND_1)',
'defined(TEST_IF_COND_2)'] } ] }
+{ 'struct': 'CondFeatureStruct4',
+ 'data': { 'foo': 'int' },
+ 'features': [ { 'name': 'feature1', 'if': {'any': ['defined(TEST_IF_COND_1)',
+ 'defined(TEST_IF_COND_2)'] } } ] }
{ 'enum': 'FeatureEnum1',
'data': [ 'eins', 'zwei', 'drei' ],
@@ -313,7 +316,8 @@
'*fs4': 'FeatureStruct4',
'*cfs1': 'CondFeatureStruct1',
'*cfs2': 'CondFeatureStruct2',
- '*cfs3': 'CondFeatureStruct3' },
+ '*cfs3': 'CondFeatureStruct3',
+ '*cfs4': 'CondFeatureStruct4' },
'returns': 'FeatureStruct1',
'features': [] }
diff --git a/tests/qapi-schema/qapi-schema-test.out b/tests/qapi-schema/qapi-schema-test.out
index c2d303aa18..f859bf648d 100644
--- a/tests/qapi-schema/qapi-schema-test.out
+++ b/tests/qapi-schema/qapi-schema-test.out
@@ -298,49 +298,49 @@ command __org.qemu_x-command q_obj___org.qemu_x-command-arg -> __org.qemu_x-Unio
object TestIfStruct
member foo: int optional=False
member bar: int optional=False
- if IfAll(['defined(TEST_IF_STRUCT_BAR)'])
- if IfAll(['defined(TEST_IF_STRUCT)'])
+ if 'defined(TEST_IF_STRUCT_BAR)'
+ if 'defined(TEST_IF_STRUCT)'
enum TestIfEnum
member foo
member bar
- if IfAll(['defined(TEST_IF_ENUM_BAR)'])
- if IfAll(['defined(TEST_IF_ENUM)'])
+ if 'defined(TEST_IF_ENUM_BAR)'
+ if 'defined(TEST_IF_ENUM)'
object q_obj_TestStruct-wrapper
member data: TestStruct optional=False
enum TestIfUnionKind
member foo
- member bar
- if IfAll(['defined(TEST_IF_UNION_BAR)'])
- if IfAll(['defined(TEST_IF_UNION) && defined(TEST_IF_STRUCT)'])
+ member union-bar
+ if 'defined(TEST_IF_UNION_BAR)'
+ if IfAll(['defined(TEST_IF_UNION)', 'defined(TEST_IF_STRUCT)'])
object TestIfUnion
member type: TestIfUnionKind optional=False
tag type
case foo: q_obj_TestStruct-wrapper
- case bar: q_obj_str-wrapper
- if IfAll(['defined(TEST_IF_UNION_BAR)'])
- if IfAll(['defined(TEST_IF_UNION) && defined(TEST_IF_STRUCT)'])
+ case union-bar: q_obj_str-wrapper
+ if 'defined(TEST_IF_UNION_BAR)'
+ if IfAll(['defined(TEST_IF_UNION)', 'defined(TEST_IF_STRUCT)'])
object q_obj_test-if-union-cmd-arg
member union-cmd-arg: TestIfUnion optional=False
- if IfAll(['defined(TEST_IF_UNION)'])
+ if 'defined(TEST_IF_UNION)'
command test-if-union-cmd q_obj_test-if-union-cmd-arg -> None
gen=True success_response=True boxed=False oob=False preconfig=False
- if IfAll(['defined(TEST_IF_UNION)'])
+ if 'defined(TEST_IF_UNION)'
alternate TestIfAlternate
tag type
case foo: int
case bar: TestStruct
- if IfAll(['defined(TEST_IF_ALT_BAR)'])
- if IfAll(['defined(TEST_IF_ALT) && defined(TEST_IF_STRUCT)'])
+ if 'defined(TEST_IF_ALT_BAR)'
+ if IfAll(['defined(TEST_IF_ALT)', 'defined(TEST_IF_STRUCT)'])
object q_obj_test-if-alternate-cmd-arg
member alt-cmd-arg: TestIfAlternate optional=False
- if IfAll(['defined(TEST_IF_ALT)'])
+ if IfAll(['defined(TEST_IF_ALT)', IfNot('defined(TEST_IF_NOT_ALT)')])
command test-if-alternate-cmd q_obj_test-if-alternate-cmd-arg -> None
gen=True success_response=True boxed=False oob=False preconfig=False
- if IfAll(['defined(TEST_IF_ALT)'])
+ if IfAll(['defined(TEST_IF_ALT)', IfNot('defined(TEST_IF_NOT_ALT)')])
object q_obj_test-if-cmd-arg
member foo: TestIfStruct optional=False
member bar: TestIfEnum optional=False
- if IfAll(['defined(TEST_IF_CMD_BAR)'])
+ if 'defined(TEST_IF_CMD_BAR)'
if IfAll(['defined(TEST_IF_CMD)', 'defined(TEST_IF_STRUCT)'])
command test-if-cmd q_obj_test-if-cmd-arg -> UserDefThree
gen=True success_response=True boxed=False oob=False preconfig=False
@@ -348,15 +348,15 @@ command test-if-cmd q_obj_test-if-cmd-arg -> UserDefThree
command test-cmd-return-def-three None -> UserDefThree
gen=True success_response=True boxed=False oob=False preconfig=False
array TestIfEnumList TestIfEnum
- if IfAll(['defined(TEST_IF_ENUM)'])
+ if 'defined(TEST_IF_ENUM)'
object q_obj_TEST_IF_EVENT-arg
member foo: TestIfStruct optional=False
member bar: TestIfEnumList optional=False
- if IfAll(['defined(TEST_IF_EVT_BAR)'])
- if IfAll(['defined(TEST_IF_EVT) && defined(TEST_IF_STRUCT)'])
+ if 'defined(TEST_IF_EVT_BAR)'
+ if IfAll(['defined(TEST_IF_EVT)', 'defined(TEST_IF_STRUCT)'])
event TEST_IF_EVENT q_obj_TEST_IF_EVENT-arg
boxed=False
- if IfAll(['defined(TEST_IF_EVT) && defined(TEST_IF_STRUCT)'])
+ if IfAll(['defined(TEST_IF_EVT)', 'defined(TEST_IF_STRUCT)'])
object FeatureStruct0
member foo: int optional=False
object FeatureStruct1
@@ -379,17 +379,21 @@ object FeatureStruct4
object CondFeatureStruct1
member foo: int optional=False
feature feature1
- if IfAll(['defined(TEST_IF_FEATURE_1)'])
+ if 'defined(TEST_IF_FEATURE_1)'
object CondFeatureStruct2
member foo: int optional=False
feature feature1
- if IfAll(['defined(TEST_IF_FEATURE_1)'])
+ if 'defined(TEST_IF_FEATURE_1)'
feature feature2
- if IfAll(['defined(TEST_IF_FEATURE_2)'])
+ if 'defined(TEST_IF_FEATURE_2)'
object CondFeatureStruct3
member foo: int optional=False
feature feature1
if IfAll(['defined(TEST_IF_COND_1)', 'defined(TEST_IF_COND_2)'])
+object CondFeatureStruct4
+ member foo: int optional=False
+ feature feature1
+ if IfAny(['defined(TEST_IF_COND_1)', 'defined(TEST_IF_COND_2)'])
enum FeatureEnum1
member eins
member zwei
@@ -417,6 +421,7 @@ object q_obj_test-features0-arg
member cfs1: CondFeatureStruct1 optional=True
member cfs2: CondFeatureStruct2 optional=True
member cfs3: CondFeatureStruct3 optional=True
+ member cfs4: CondFeatureStruct4 optional=True
command test-features0 q_obj_test-features0-arg -> FeatureStruct1
gen=True success_response=True boxed=False oob=False preconfig=False
command test-command-features1 None -> None
@@ -429,13 +434,13 @@ command test-command-features3 None -> None
command test-command-cond-features1 None -> None
gen=True success_response=True boxed=False oob=False preconfig=False
feature feature1
- if IfAll(['defined(TEST_IF_FEATURE_1)'])
+ if 'defined(TEST_IF_FEATURE_1)'
command test-command-cond-features2 None -> None
gen=True success_response=True boxed=False oob=False preconfig=False
feature feature1
- if IfAll(['defined(TEST_IF_FEATURE_1)'])
+ if 'defined(TEST_IF_FEATURE_1)'
feature feature2
- if IfAll(['defined(TEST_IF_FEATURE_2)'])
+ if 'defined(TEST_IF_FEATURE_2)'
command test-command-cond-features3 None -> None
gen=True success_response=True boxed=False oob=False preconfig=False
feature feature1
diff --git a/tests/qapi-schema/struct-member-if-invalid.err b/tests/qapi-schema/struct-member-if-invalid.err
index 42e7fdae3c..c18157c1f9 100644
--- a/tests/qapi-schema/struct-member-if-invalid.err
+++ b/tests/qapi-schema/struct-member-if-invalid.err
@@ -1,2 +1,2 @@
struct-member-if-invalid.json: In struct 'Stru':
-struct-member-if-invalid.json:2: 'if' condition of 'data' member 'member' must be a string or a list of strings
+struct-member-if-invalid.json:2: 'if' condition of 'data' member 'member' must be a string, a list of strings or a dict
--
2.29.0
next prev parent reply other threads:[~2021-04-29 13:47 UTC|newest]
Thread overview: 40+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-04-29 13:40 [PATCH v3 0/9] qapi: untie 'if' conditions from C preprocessor marcandre.lureau
2021-04-29 13:40 ` [PATCH v3 1/9] qapi: replace List[str] by QAPISchemaIfCond marcandre.lureau
2021-05-12 20:53 ` John Snow
2021-05-17 11:17 ` Marc-André Lureau
2021-05-17 16:30 ` John Snow
2021-04-29 13:40 ` [PATCH v3 2/9] qapi: move gen_if/gen_endif to QAPISchemaIfCond marcandre.lureau
2021-05-11 16:39 ` Stefan Hajnoczi
2021-05-12 12:36 ` Marc-André Lureau
2021-05-12 18:53 ` John Snow
2021-05-17 11:18 ` Marc-André Lureau
2021-05-12 21:01 ` John Snow
2021-05-21 14:35 ` Markus Armbruster
2021-04-29 13:40 ` [PATCH v3 3/9] qapi: start building an 'if' predicate tree marcandre.lureau
2021-05-12 21:39 ` John Snow
2021-05-17 11:18 ` Marc-André Lureau
2021-05-17 16:34 ` John Snow
2021-05-21 14:48 ` Markus Armbruster
2021-05-21 16:18 ` John Snow
2021-06-08 13:57 ` Markus Armbruster
2021-04-29 13:40 ` [PATCH v3 4/9] qapi: introduce IfPredicateList and IfAny marcandre.lureau
2021-05-12 23:26 ` John Snow
2021-05-17 11:18 ` Marc-André Lureau
2021-05-17 16:35 ` John Snow
2021-04-29 13:40 ` [PATCH v3 5/9] qapi: add IfNot marcandre.lureau
2021-05-12 23:32 ` John Snow
2021-04-29 13:40 ` marcandre.lureau [this message]
2021-05-12 23:47 ` [PATCH v3 6/9] qapi: normalize 'if' condition to IfPredicate tree John Snow
2021-05-17 11:18 ` Marc-André Lureau
2021-05-17 16:41 ` John Snow
2021-04-29 13:40 ` [PATCH v3 7/9] qapi: convert 'if' C-expressions to the new syntax tree marcandre.lureau
2021-05-12 23:51 ` John Snow
2021-05-17 11:20 ` Marc-André Lureau
2021-04-29 13:40 ` [PATCH v3 8/9] qapi: make 'if' condition strings simple identifiers marcandre.lureau
2021-05-12 23:56 ` John Snow
2021-04-29 13:40 ` [PATCH v3 9/9] docs: update the documentation about schema configuration marcandre.lureau
2021-05-13 0:01 ` John Snow
2021-05-11 16:52 ` [PATCH v3 0/9] qapi: untie 'if' conditions from C preprocessor Stefan Hajnoczi
2021-05-12 12:39 ` Marc-André Lureau
2021-05-12 17:43 ` Markus Armbruster
2021-05-12 18:58 ` John Snow
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20210429134032.1125111-7-marcandre.lureau@redhat.com \
--to=marcandre.lureau@redhat.com \
--cc=armbru@redhat.com \
--cc=jsnow@redhat.com \
--cc=qemu-devel@nongnu.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.