From: Mauro Carvalho Chehab <mchehab+samsung@kernel.org>
To: Hans Verkuil <hverkuil@xs4all.nl>
Cc: linux-media@vger.kernel.org, Hans Verkuil <hans.verkuil@cisco.com>
Subject: Re: [RFC PATCH 2/3] media: add support for properties
Date: Fri, 3 Aug 2018 14:01:12 -0300 [thread overview]
Message-ID: <20180803140112.4fd5ba78@coco.lan> (raw)
In-Reply-To: <20180803143626.48191-3-hverkuil@xs4all.nl>
Em Fri, 3 Aug 2018 16:36:25 +0200
Hans Verkuil <hverkuil@xs4all.nl> escreveu:
> From: Hans Verkuil <hans.verkuil@cisco.com>
>
> Initially support u64/s64 and string properties.
>
> Signed-off-by: Hans Verkuil <hans.verkuil@cisco.com>
> ---
> drivers/media/media-device.c | 98 +++++++++++++++++++-
> drivers/media/media-entity.c | 65 +++++++++++++
> include/media/media-device.h | 6 ++
> include/media/media-entity.h | 172 +++++++++++++++++++++++++++++++++++
> 4 files changed, 338 insertions(+), 3 deletions(-)
>
> diff --git a/drivers/media/media-device.c b/drivers/media/media-device.c
> index fcdf3d5dc4b6..6fa9555a669f 100644
> --- a/drivers/media/media-device.c
> +++ b/drivers/media/media-device.c
> @@ -241,10 +241,15 @@ static long media_device_get_topology(struct media_device *mdev, void *arg)
> struct media_interface *intf;
> struct media_pad *pad;
> struct media_link *link;
> + struct media_prop *prop;
> struct media_v2_entity kentity, __user *uentity;
> struct media_v2_interface kintf, __user *uintf;
> struct media_v2_pad kpad, __user *upad;
> struct media_v2_link klink, __user *ulink;
> + struct media_v2_prop kprop, __user *uprop;
> + void __user *uprop_payload;
> + unsigned int payload_size = 0;
> + unsigned int payload_offset = 0;
> unsigned int i;
> int ret = 0;
>
> @@ -374,6 +379,73 @@ static long media_device_get_topology(struct media_device *mdev, void *arg)
> topo->num_links = i;
> topo->reserved4 = 0;
>
> + /* Get properties and number of properties */
> + i = 0;
> + uprop = media_get_uptr(topo->ptr_props);
> + payload_offset = topo->num_props * sizeof(*uprop);
> + media_device_for_each_prop(prop, mdev) {
> + payload_size += prop->payload_size;
> + i++;
> +
> + if (ret || !uprop)
> + continue;
I would add here filter capabilities to to the objects requested by
the ioctl, e. g. something similar to this:
bool return_prop = false;
enum media_gobj_type type = media_type(prop->graph_obj.id);
/*
* If only props are required, don't filter them.
* Otherwise, filter properties according with the types requested
* by G_TOPOLOGY call
*/
if (!(uentity || uintf || upad || ulink))
return_prop = true;
else if (uentity && type == MEDIA_GRAPH_ENTITY)
return_prop = true;
else if (uintf && type == MEDIA_GRAPH_INTF_DEVNODE)
return_prop = true;
else if (upad && type == MEDIA_GRAPH_PAD)
return_prop = true;
else if (link && type == MEDIA_GRAPH_LINK)
return_prop = true;
if (return_prop) {
/* the code that copy properties below */
}
> +
> + if (i > topo->num_props) {
> + ret = -ENOSPC;
> + continue;
> + }
> +
> + memset(&kprop, 0, sizeof(kprop));
> +
> + /* Copy prop fields to userspace struct */
> + kprop.id = prop->graph_obj.id;
> + kprop.owner_id = prop->owner->id;
> + kprop.type = prop->type;
> + kprop.flags = 0;
> + kprop.payload_size = prop->payload_size;
> + if (kprop.payload_size)
> + kprop.payload_offset = payload_offset +
> + payload_size - prop->payload_size;
> + else
> + kprop.payload_offset = 0;
> + payload_offset -= sizeof(*uprop);
> + memcpy(kprop.name, prop->name, sizeof(kprop.name));
> + kprop.uval = prop->uval;
> +
> + if (copy_to_user(uprop, &kprop, sizeof(kprop)))
> + ret = -EFAULT;
> + uprop++;
This way, properties will be filtered according with userspace's needs.
> + }
> + topo->num_props = i;
> + if (uprop && topo->props_payload_size < payload_size)
> + ret = -ENOSPC;
> + topo->props_payload_size = payload_size;
> + if (!uprop || ret)
> + return ret;
> +
> + uprop_payload = uprop;
> + media_device_for_each_prop(prop, mdev) {
> + i++;
> +
> + if (!prop->payload_size)
> + continue;
You would need a similar test here too.
> +
> + if (copy_to_user(uprop_payload, prop->string, prop->payload_size))
> + return -EFAULT;
> + uprop_payload += prop->payload_size;
> + }
> +
> + return 0;
> +}
> +
> +static long media_device_get_topology_1(struct media_device *mdev, void *arg)
> +{
> + struct media_v2_topology topo = {};
> + long ret;
> +
> + memcpy(&topo, arg, sizeof(struct media_v2_topology_1));
> + ret = media_device_get_topology(mdev, &topo);
> + memcpy(arg, &topo, sizeof(struct media_v2_topology_1));
> return ret;
> }
Nah, no need that.
>
> @@ -424,6 +496,7 @@ static const struct media_ioctl_info ioctl_info[] = {
> MEDIA_IOC(ENUM_ENTITIES, media_device_enum_entities, MEDIA_IOC_FL_GRAPH_MUTEX),
> MEDIA_IOC(ENUM_LINKS, media_device_enum_links, MEDIA_IOC_FL_GRAPH_MUTEX),
> MEDIA_IOC(SETUP_LINK, media_device_setup_link, MEDIA_IOC_FL_GRAPH_MUTEX),
> + MEDIA_IOC(G_TOPOLOGY_1, media_device_get_topology_1, MEDIA_IOC_FL_GRAPH_MUTEX),
> MEDIA_IOC(G_TOPOLOGY, media_device_get_topology, MEDIA_IOC_FL_GRAPH_MUTEX),
> };
Nah, need for that!
Sakari made once a patch changing this struct to allow variable params.
Basically, he added a minimum and maximum size at the struct.
See it at:
https://patchwork.kernel.org/patch/9012371/
>
> @@ -438,12 +511,12 @@ static long media_device_ioctl(struct file *filp, unsigned int cmd,
> long ret;
>
> if (_IOC_NR(cmd) >= ARRAY_SIZE(ioctl_info)
> - || ioctl_info[_IOC_NR(cmd)].cmd != cmd)
> + || (ioctl_info[_IOC_NR(cmd)].cmd != cmd))
> return -ENOIOCTLCMD;
>
> info = &ioctl_info[_IOC_NR(cmd)];
>
> - if (_IOC_SIZE(info->cmd) > sizeof(__karg)) {
> + if (_IOC_SIZE(cmd) > sizeof(__karg)) {
> karg = kmalloc(_IOC_SIZE(info->cmd), GFP_KERNEL);
> if (!karg)
> return -ENOMEM;
> @@ -582,6 +655,7 @@ int __must_check media_device_register_entity(struct media_device *mdev,
> WARN_ON(entity->graph_obj.mdev != NULL);
> entity->graph_obj.mdev = mdev;
> INIT_LIST_HEAD(&entity->links);
> + INIT_LIST_HEAD(&entity->props);
> entity->num_links = 0;
> entity->num_backlinks = 0;
>
> @@ -635,6 +709,18 @@ int __must_check media_device_register_entity(struct media_device *mdev,
> }
> EXPORT_SYMBOL_GPL(media_device_register_entity);
>
> +static void media_device_free_props(struct list_head *list)
> +{
> + while (!list_empty(list)) {
> + struct media_prop *prop;
> +
> + prop = list_first_entry(list, struct media_prop, list);
> + list_del(&prop->list);
> + media_gobj_destroy(&prop->graph_obj);
> + kfree(prop);
> + }
> +}
> +
> static void __media_device_unregister_entity(struct media_entity *entity)
> {
> struct media_device *mdev = entity->graph_obj.mdev;
> @@ -656,8 +742,13 @@ static void __media_device_unregister_entity(struct media_entity *entity)
> __media_entity_remove_links(entity);
>
> /* Remove all pads that belong to this entity */
> - for (i = 0; i < entity->num_pads; i++)
> + for (i = 0; i < entity->num_pads; i++) {
> + media_device_free_props(&entity->pads[i].props);
> media_gobj_destroy(&entity->pads[i].graph_obj);
> + }
> +
> + /* Remove all props that belong to this entity */
> + media_device_free_props(&entity->props);
>
> /* Remove the entity */
> media_gobj_destroy(&entity->graph_obj);
> @@ -696,6 +787,7 @@ void media_device_init(struct media_device *mdev)
> INIT_LIST_HEAD(&mdev->interfaces);
> INIT_LIST_HEAD(&mdev->pads);
> INIT_LIST_HEAD(&mdev->links);
> + INIT_LIST_HEAD(&mdev->props);
> INIT_LIST_HEAD(&mdev->entity_notify);
> mutex_init(&mdev->graph_mutex);
> ida_init(&mdev->entity_internal_idx);
> diff --git a/drivers/media/media-entity.c b/drivers/media/media-entity.c
> index 3498551e618e..5c09f1937bc9 100644
> --- a/drivers/media/media-entity.c
> +++ b/drivers/media/media-entity.c
> @@ -34,6 +34,8 @@ static inline const char *gobj_type(enum media_gobj_type type)
> return "link";
> case MEDIA_GRAPH_INTF_DEVNODE:
> return "intf-devnode";
> + case MEDIA_GRAPH_PROP:
> + return "prop";
> default:
> return "unknown";
> }
> @@ -147,6 +149,16 @@ static void dev_dbg_obj(const char *event_name, struct media_gobj *gobj)
> devnode->major, devnode->minor);
> break;
> }
> + case MEDIA_GRAPH_PROP:
> + {
> + struct media_prop *prop = gobj_to_prop(gobj);
> +
> + dev_dbg(gobj->mdev->dev,
> + "%s id %u: prop '%s':%u[%u]\n",
> + event_name, media_id(gobj),
> + prop->name, media_id(prop->owner), prop->index);
> + break;
> + }
> }
> #endif
> }
> @@ -175,6 +187,9 @@ void media_gobj_create(struct media_device *mdev,
> case MEDIA_GRAPH_INTF_DEVNODE:
> list_add_tail(&gobj->list, &mdev->interfaces);
> break;
> + case MEDIA_GRAPH_PROP:
> + list_add_tail(&gobj->list, &mdev->props);
> + break;
> }
>
> mdev->topology_version++;
> @@ -212,6 +227,7 @@ int media_entity_pads_init(struct media_entity *entity, u16 num_pads,
> if (num_pads >= MEDIA_ENTITY_MAX_PADS)
> return -E2BIG;
>
> + INIT_LIST_HEAD(&entity->props);
> entity->num_pads = num_pads;
> entity->pads = pads;
As I said before, I long term the best is to use internally a tree model,
and attach the property to the object id, instead of adding it to each
type of entity, but for the first version, this works.
I very much prefer to start with a sub-optimal internal model but have
something to merge than wait forever to have a complex schema.
>
> @@ -221,6 +237,7 @@ int media_entity_pads_init(struct media_entity *entity, u16 num_pads,
> for (i = 0; i < num_pads; i++) {
> pads[i].entity = entity;
> pads[i].index = i;
> + INIT_LIST_HEAD(&pads[i].props);
> if (mdev)
> media_gobj_create(mdev, MEDIA_GRAPH_PAD,
> &entity->pads[i].graph_obj);
> @@ -233,6 +250,54 @@ int media_entity_pads_init(struct media_entity *entity, u16 num_pads,
> }
> EXPORT_SYMBOL_GPL(media_entity_pads_init);
>
> +static struct media_prop *media_create_prop(struct media_gobj *owner, u32 type,
> + const char *name, u64 val, const void *ptr, u32 payload_size)
> +{
> + struct media_prop *prop = kzalloc(sizeof(*prop) + payload_size,
> + GFP_KERNEL);
> +
> + if (!prop)
> + return NULL;
> + prop->type = type;
> + strlcpy(prop->name, name, sizeof(prop->name));
> + media_gobj_create(owner->mdev, MEDIA_GRAPH_PROP, &prop->graph_obj);
> + prop->owner = owner;
> + if (payload_size) {
> + prop->string = (char *)&prop[1];
> + memcpy(prop->string, ptr, payload_size);
> + prop->payload_size = payload_size;
> + } else {
> + prop->uval = val;
> + }
> + return prop;
> +}
> +
> +int media_entity_add_prop(struct media_entity *ent, u32 type,
> + const char *name, u64 val, const void *ptr, u32 payload_size)
> +{
> + struct media_prop *prop = media_create_prop(&ent->graph_obj, type,
> + name, val, ptr, payload_size);
> +
> + if (!prop)
> + return -ENOMEM;
> + list_add_tail(&prop->list, &ent->props);
> + return 0;
> +}
> +EXPORT_SYMBOL_GPL(media_entity_add_prop);
> +
> +int media_pad_add_prop(struct media_pad *pad, u32 type,
> + const char *name, u64 val, const void *ptr, u32 payload_size)
> +{
> + struct media_prop *prop = media_create_prop(&pad->graph_obj, type,
> + name, val, ptr, payload_size);
> +
> + if (!prop)
> + return -ENOMEM;
> + list_add_tail(&prop->list, &pad->props);
> + return 0;
> +}
> +EXPORT_SYMBOL_GPL(media_pad_add_prop);
> +
> /* -----------------------------------------------------------------------------
> * Graph traversal
> */
> diff --git a/include/media/media-device.h b/include/media/media-device.h
> index bcc6ec434f1f..a30a931df00b 100644
> --- a/include/media/media-device.h
> +++ b/include/media/media-device.h
> @@ -78,6 +78,7 @@ struct media_device_ops {
> * @interfaces: List of registered interfaces
> * @pads: List of registered pads
> * @links: List of registered links
> + * @props: List of registered properties
> * @entity_notify: List of registered entity_notify callbacks
> * @graph_mutex: Protects access to struct media_device data
> * @pm_count_walk: Graph walk for power state walk. Access serialised using
> @@ -144,6 +145,7 @@ struct media_device {
> struct list_head interfaces;
> struct list_head pads;
> struct list_head links;
> + struct list_head props;
>
> /* notify callback list invoked when a new entity is registered */
> struct list_head entity_notify;
> @@ -382,6 +384,10 @@ void media_device_unregister_entity_notify(struct media_device *mdev,
> #define media_device_for_each_link(link, mdev) \
> list_for_each_entry(link, &(mdev)->links, graph_obj.list)
>
> +/* Iterate over all props. */
> +#define media_device_for_each_prop(prop, mdev) \
> + list_for_each_entry(prop, &(mdev)->props, graph_obj.list)
> +
> /**
> * media_device_pci_init() - create and initialize a
> * struct &media_device from a PCI device.
> diff --git a/include/media/media-entity.h b/include/media/media-entity.h
> index 3aa3d58d1d58..01b13809656d 100644
> --- a/include/media/media-entity.h
> +++ b/include/media/media-entity.h
> @@ -36,12 +36,14 @@
> * @MEDIA_GRAPH_LINK: Identify a media link
> * @MEDIA_GRAPH_INTF_DEVNODE: Identify a media Kernel API interface via
> * a device node
> + * @MEDIA_GRAPH_PROP: Identify a media property
> */
> enum media_gobj_type {
> MEDIA_GRAPH_ENTITY,
> MEDIA_GRAPH_PAD,
> MEDIA_GRAPH_LINK,
> MEDIA_GRAPH_INTF_DEVNODE,
> + MEDIA_GRAPH_PROP,
> };
>
> #define MEDIA_BITS_PER_TYPE 8
> @@ -164,12 +166,44 @@ struct media_link {
> * @flags: Pad flags, as defined in
> * :ref:`include/uapi/linux/media.h <media_header>`
> * (seek for ``MEDIA_PAD_FL_*``)
> + * @num_props: Number of pad properties
> + * @props: The list pad properties
> */
> struct media_pad {
> struct media_gobj graph_obj; /* must be first field in struct */
> struct media_entity *entity;
> u16 index;
> unsigned long flags;
> + u16 num_props;
> + struct list_head props;
> +};
> +
> +/**
> + * struct media_prop - A media property graph object.
> + *
> + * @graph_obj: Embedded structure containing the media object common data
> + * @list: Linked list associated with the object that owns the link.
> + * @owner: Graph object this property belongs to
> + * @index: Property index for the owner property array, numbered from 0 to n
> + * @type: Property type
> + * @payload_size: Property payload size (i.e. additional bytes beyond this struct)
> + * @name: Property name
> + * @uval: Property value (unsigned)
> + * @sval: Property value (signed)
> + * @string: Property string value
> + */
> +struct media_prop {
> + struct media_gobj graph_obj; /* must be first field in struct */
> + struct list_head list;
> + struct media_gobj *owner;
> + u32 type;
> + u32 payload_size;
> + char name[32];
> + union {
> + u64 uval;
> + s64 sval;
> + char *string;
> + };
> };
>
> /**
> @@ -239,10 +273,12 @@ enum media_entity_type {
> * @num_pads: Number of sink and source pads.
> * @num_links: Total number of links, forward and back, enabled and disabled.
> * @num_backlinks: Number of backlinks
> + * @num_props: Number of entity properties.
> * @internal_idx: An unique internal entity specific number. The numbers are
> * re-used if entities are unregistered or registered again.
> * @pads: Pads array with the size defined by @num_pads.
> * @links: List of data links.
> + * @props: List of entity properties.
> * @ops: Entity operations.
> * @stream_count: Stream count for the entity.
> * @use_count: Use count for the entity.
> @@ -274,10 +310,12 @@ struct media_entity {
> u16 num_pads;
> u16 num_links;
> u16 num_backlinks;
> + u16 num_props;
> int internal_idx;
>
> struct media_pad *pads;
> struct list_head links;
> + struct list_head props;
>
> const struct media_entity_operations *ops;
>
> @@ -565,6 +603,15 @@ static inline bool media_entity_enum_intersects(
> #define gobj_to_intf(gobj) \
> container_of(gobj, struct media_interface, graph_obj)
>
> +/**
> + * gobj_to_prop - returns the struct &media_prop pointer from the
> + * @gobj contained on it.
> + *
> + * @gobj: Pointer to the struct &media_gobj graph object
> + */
> +#define gobj_to_prop(gobj) \
> + container_of(gobj, struct media_prop, graph_obj)
> +
> /**
> * intf_to_devnode - returns the struct media_intf_devnode pointer from the
> * @intf contained on it.
> @@ -727,6 +774,131 @@ int media_create_pad_links(const struct media_device *mdev,
>
> void __media_entity_remove_links(struct media_entity *entity);
>
> +/**
> + * media_entity_add_prop() - Add property to entity
> + *
> + * @entity: entity where to add the property
> + * @type: property type
> + * @name: property name
> + * @val: property value: use if payload_size == 0
> + * @ptr: property pointer to payload
> + * @payload_size: property payload size
> + *
> + * Returns 0 on success, or an error on failure.
> + */
> +int media_entity_add_prop(struct media_entity *ent, u32 type,
> + const char *name, u64 val, const void *ptr, u32 payload_size);
> +
> +/**
> + * media_pad_add_prop() - Add property to pad
> + *
> + * @pad: pad where to add the property
> + * @type: property type
> + * @name: property name
> + * @val: property value: use if payload_size == 0
> + * @ptr: property pointer to payload
> + * @payload_size: property payload size
> + *
> + * Returns 0 on success, or an error on failure.
> + */
> +int media_pad_add_prop(struct media_pad *pad, u32 type,
> + const char *name, u64 val, const void *ptr, u32 payload_size);
> +
> +/**
> + * media_entity_add_prop_u64() - Add u64 property to entity
> + *
> + * @entity: entity where to add the property
> + * @name: property name
> + * @val: property value
> + *
> + * Returns 0 on success, or an error on failure.
> + */
> +static inline int media_entity_add_prop_u64(struct media_entity *entity,
> + const char *name, u64 val)
> +{
> + return media_entity_add_prop(entity, MEDIA_PROP_TYPE_U64,
> + name, val, NULL, 0);
> +}
> +
> +/**
> + * media_entity_add_prop_s64() - Add s64 property to entity
> + *
> + * @entity: entity where to add the property
> + * @name: property name
> + * @val: property value
> + *
> + * Returns 0 on success, or an error on failure.
> + */
> +static inline int media_entity_add_prop_s64(struct media_entity *entity,
> + const char *name, s64 val)
> +{
> + return media_entity_add_prop(entity, MEDIA_PROP_TYPE_S64,
> + name, (u64)val, NULL, 0);
> +}
> +
> +/**
> + * media_entity_add_prop_string() - Add string property to entity
> + *
> + * @entity: entity where to add the property
> + * @name: property name
> + * @string: property string value
> + *
> + * Returns 0 on success, or an error on failure.
> + */
> +static inline int media_entity_add_prop_string(struct media_entity *entity,
> + const char *name, const char *string)
> +{
> + return media_entity_add_prop(entity, MEDIA_PROP_TYPE_STRING,
> + name, 0, string, strlen(string) + 1);
> +}
> +
> +/**
> + * media_pad_add_prop_u64() - Add u64 property to pad
> + *
> + * @pad: pad where to add the property
> + * @name: property name
> + * @val: property value
> + *
> + * Returns 0 on success, or an error on failure.
> + */
> +static inline int media_pad_add_prop_u64(struct media_pad *pad,
> + const char *name, u64 val)
> +{
> + return media_pad_add_prop(pad, MEDIA_PROP_TYPE_U64, name, val, NULL, 0);
> +}
> +
> +/**
> + * media_pad_add_prop_s64() - Add s64 property to pad
> + *
> + * @pad: pad where to add the property
> + * @name: property name
> + * @val: property value
> + *
> + * Returns 0 on success, or an error on failure.
> + */
> +static inline int media_pad_add_prop_s64(struct media_pad *pad,
> + const char *name, s64 val)
> +{
> + return media_pad_add_prop(pad, MEDIA_PROP_TYPE_S64,
> + name, (u64)val, NULL, 0);
> +}
> +
> +/**
> + * media_pad_add_prop_string() - Add string property to pad
> + *
> + * @pad: pad where to add the property
> + * @name: property name
> + * @string: property string value
> + *
> + * Returns 0 on success, or an error on failure.
> + */
> +static inline int media_pad_add_prop_string(struct media_pad *pad,
> + const char *name, const char *string)
> +{
> + return media_pad_add_prop(pad, MEDIA_PROP_TYPE_STRING,
> + name, 0, string, strlen(string) + 1);
> +}
> +
> /**
> * media_entity_remove_links() - remove all links associated with an entity
> *
Thanks,
Mauro
next prev parent reply other threads:[~2018-08-03 18:58 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2018-08-03 14:36 [RFC PATCH 0/3] Media Controller Properties Hans Verkuil
2018-08-03 14:36 ` [RFC PATCH 1/3] uapi/linux/media.h: add property support Hans Verkuil
2018-08-03 15:48 ` Mauro Carvalho Chehab
2018-08-03 14:36 ` [RFC PATCH 2/3] media: add support for properties Hans Verkuil
2018-08-03 17:01 ` Mauro Carvalho Chehab [this message]
2018-08-03 14:36 ` [RFC PATCH 3/3] vimc: add test properties Hans Verkuil
2018-08-03 15:03 ` [RFC PATCH 0/3] Media Controller Properties Hans Verkuil
2018-08-03 15:23 ` Mauro Carvalho Chehab
2018-08-06 11:08 ` Hans Verkuil
2018-11-14 8:09 ` Tomasz Figa
2018-11-14 8:19 ` Hans Verkuil
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=20180803140112.4fd5ba78@coco.lan \
--to=mchehab+samsung@kernel.org \
--cc=hans.verkuil@cisco.com \
--cc=hverkuil@xs4all.nl \
--cc=linux-media@vger.kernel.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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox