From: Tobias Waldekranz <tobias@waldekranz.com>
To: Ahmad Fatoum <a.fatoum@pengutronix.de>, barebox@lists.infradead.org
Subject: Re: [PATCH 2/5] dm: Add initial device mapper infrastructure
Date: Mon, 08 Sep 2025 11:27:24 +0200 [thread overview]
Message-ID: <87ldmp2t1f.fsf@waldekranz.com> (raw)
In-Reply-To: <8c5841c8-cbe4-4aab-8c5b-c75f119f5bc0@pengutronix.de>
On fre, sep 05, 2025 at 18:14, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> Hi,
>
> On 8/28/25 5:05 PM, Tobias Waldekranz wrote:
>> Add initial scaffolding for a block device mapper which is intended to
>> be compatible with the corresponding subsystem in Linux.
>>
>> This is the foundation of several higher level abstractions, for
>> example:
>>
>> - LVM: Linux Volume manager. Dynamically allocates logical volumes
>> from one or more storage devices, manages RAID arrays, etc.
>>
>> - LUKS: Linux Unified Key Setup. Transparent disk
>> encryption/decryption.
>>
>> - dm-verity: Transparent integrity checking of block devices.
>>
>> Being able to configure dm devices in barebox will allow us to access
>> data from LVM volumes, access filesystems with block layer integrity
>> validation, etc.
>>
>> Signed-off-by: Tobias Waldekranz <tobias@waldekranz.com>
>> ---
>> drivers/block/Kconfig | 2 +
>> drivers/block/Makefile | 1 +
>> drivers/block/dm/Kconfig | 7 +
>> drivers/block/dm/Makefile | 2 +
>> drivers/block/dm/dm-core.c | 393 +++++++++++++++++++++++++++++++++++
>> drivers/block/dm/dm-target.h | 39 ++++
>> include/dm.h | 16 ++
>> 7 files changed, 460 insertions(+)
>> create mode 100644 drivers/block/dm/Kconfig
>> create mode 100644 drivers/block/dm/Makefile
>> create mode 100644 drivers/block/dm/dm-core.c
>> create mode 100644 drivers/block/dm/dm-target.h
>> create mode 100644 include/dm.h
>>
>> diff --git a/drivers/block/Kconfig b/drivers/block/Kconfig
>> index 5b1b778917..dace861670 100644
>> --- a/drivers/block/Kconfig
>> +++ b/drivers/block/Kconfig
>> @@ -32,3 +32,5 @@ config RAMDISK_BLK
>> help
>> This symbol is selected by testing code that requires lightweight
>> creation of anonymous block devices backed fully by memory buffers.
>> +
>> +source "drivers/block/dm/Kconfig"
>> diff --git a/drivers/block/Makefile b/drivers/block/Makefile
>> index 6066b35c31..8f913bd0ad 100644
>> --- a/drivers/block/Makefile
>> +++ b/drivers/block/Makefile
>> @@ -2,3 +2,4 @@
>> obj-$(CONFIG_EFI_BLK) += efi-block-io.o
>> obj-$(CONFIG_VIRTIO_BLK) += virtio_blk.o
>> obj-$(CONFIG_RAMDISK_BLK) += ramdisk.o
>> +obj-y += dm/
>> diff --git a/drivers/block/dm/Kconfig b/drivers/block/dm/Kconfig
>> new file mode 100644
>> index 0000000000..f64c69e03d
>> --- /dev/null
>> +++ b/drivers/block/dm/Kconfig
>> @@ -0,0 +1,7 @@
>> +menuconfig DM_BLK
>> + bool "Device mapper"
>> + help
>
> Spaces/Tabs mixed up.
>
>> + Composable virtual block devices made up of mappings to
>> + other data sources. Modeled after, and intended to be
>> + compatible with, the Linux kernel's device mapper subsystem.
>> +
>> diff --git a/drivers/block/dm/Makefile b/drivers/block/dm/Makefile
>> new file mode 100644
>> index 0000000000..9c045087c0
>> --- /dev/null
>> +++ b/drivers/block/dm/Makefile
>> @@ -0,0 +1,2 @@
>> +# SPDX-License-Identifier: GPL-2.0-only
>> +obj-$(CONFIG_DM_BLK) += dm-core.o
>> diff --git a/drivers/block/dm/dm-core.c b/drivers/block/dm/dm-core.c
>> new file mode 100644
>> index 0000000000..cf92dac94c
>> --- /dev/null
>> +++ b/drivers/block/dm/dm-core.c
>> @@ -0,0 +1,393 @@
>> +// SPDX-License-Identifier: GPL-2.0-only
>> +// SPDX-FileCopyrightText: © 2025 Tobias Waldekranz <tobias@waldekranz.com>, Wires
>> +
>> +#include <block.h>
>> +#include <common.h>
>> +#include <disks.h>
>> +#include <dm.h>
>> +#include <string.h>
>> +
>> +#include "dm-target.h"
>> +
>> +static LIST_HEAD(dm_target_ops_list);
>> +
>> +static struct dm_target_ops *dm_target_ops_find(const char *name)
>> +{
>> + struct dm_target_ops *ops;
>> +
>> + list_for_each_entry(ops, &dm_target_ops_list, list) {
>> + if (!strcmp(ops->name, name))
>> + return ops;
>> + }
>> + return NULL;
>> +}
>> +
>> +int dm_target_register(struct dm_target_ops *ops)
>> +{
>> + list_add(&ops->list, &dm_target_ops_list);
>> + return 0;
>> +}
>> +
>> +void dm_target_unregister(struct dm_target_ops *ops)
>> +{
>> + list_del(&ops->list);
>> +}
>> +
>> +struct dm_device {
>> + struct device dev;
>> + struct block_device blk;
>> + struct list_head list;
>> + struct list_head targets;
>> +};
>> +
>> +static LIST_HEAD(dm_device_list);
>
> I think DEFINE_DEV_CLASS would be more fitting here. Can also be listed
> with the class command this way.
Nice. Yes, that is much better.
>> +
>> +struct dm_device *dm_find_by_name(const char *name)
>> +{
>> + struct dm_device *dm;
>> +
>> + list_for_each_entry(dm, &dm_device_list, list) {
>> + if (!strcmp(dev_name(&dm->dev), name))
>> + return dm;
>> + }
>> +
>> + return ERR_PTR(-ENOENT);
>> +}
>> +EXPORT_SYMBOL(dm_find_by_name);
>> +
>> +int dm_foreach(int (*cb)(struct dm_device *dm, void *ctx), void *ctx)
>> +{
>> + struct dm_device *dm;
>> + int err;
>> +
>> + list_for_each_entry(dm, &dm_device_list, list) {
>> + err = cb(dm, ctx);
>> + if (err)
>> + return err;
>> + }
>> +
>> + return 0;
>> +}
>> +EXPORT_SYMBOL(dm_foreach);
>> +
>> +void dm_target_err(struct dm_target *ti, const char *fmt, ...)
>> +{
>> + struct dm_target *iter;
>> + va_list ap;
>> + char *msg;
>> + int i = 0;
>> +> + list_for_each_entry(iter, &ti->dm->targets, list) {
>> + if (iter == ti)
>> + break;
>> + i++;
>> + }
>> +
>> + va_start(ap, fmt);
>> + msg = xvasprintf(fmt, ap);
>> + va_end(ap);
>> +
>> + dev_err(&ti->dm->dev, "%d(%s): %s", i, ti->ops->name, msg);
>
> If someone looks at this code, because they have seen this error, they
> won't necessarily know how to interpret the %d.
My intention was that it could be correlated with the first column from
the table generated by dm_asprint()...
> Can you either add a comment above the iteration or rename the i to a
> more telling name?
...i.e., it is just an index (i). I'll add a comment describing that.
>> + free(msg);
>> +}
>> +EXPORT_SYMBOL(dm_target_err);
>> +
>> +static int dm_blk_read(struct block_device *blk, void *buf,
>> + sector_t block, blkcnt_t num_blocks)
>> +{
>> + struct dm_device *dm = container_of(blk, struct dm_device, blk);
>> + struct dm_target *ti;
>> + blkcnt_t tnblks, todo;
>> + sector_t tblk;
>> + int err;
>> +
>> + todo = num_blocks;
>> +
>> + list_for_each_entry(ti, &dm->targets, list) {
>
> Took me a while to understand this. I think a comment would be nice, e.g.:
Agreed.
> /* We can have multiple non-overlapping targets and a read may span
> * multiple targets. Fortunately, targets are ordered by base address,
> * so we iterate until we find the first applicable target and read on.
> */
>
> Or something like that.
Great, thanks!
>
>> + if (block < ti->base || block >= ti->base + ti->size)
>> + continue;
>> +
>> + if (!ti->ops->read)
>> + return -EIO;
>> +
>> + tblk = block - ti->base;
>> + tnblks = min(todo, ti->size - tblk);
>> + err = ti->ops->read(ti, buf, tblk, tnblks);
>> + if (err)
>> + return err;
>> +
>> + block += tnblks;
>> + todo -= tnblks;
>> + buf += tnblks << SECTOR_SHIFT;
>
> We don't support different block sizes right now and you hardcode
> SECTOR_SIZE below anyway, but we like to pretend we could, so
> blk->blockbits would be more apt here.
I made a conscious decision to use hardcode these everywhere as Linux
defines that a DM block _is_ 512B. You see these constants all over the
place in the target implementations in the kernel as well.
>From dmsetup(8):
| Devices are created by loading a table that specifies a target for
| each sector (512 bytes) in the logical device.
So while Barebox might one day support other block sizes, I do not think
that DM ever will.
What I also should have done is to have a comment detailing this, above
the hardcoded block size. If I add this, do you think we should stick
with the constants, or change to blk->blockbits?
>> + if (!todo)
>> + return 0;
>> + }
>> +
>> + return -EIO;
>> +}
>> +
>> +static int dm_blk_write(struct block_device *blk, const void *buf,
>> + sector_t block, blkcnt_t num_blocks)
>> +{
>> + struct dm_device *dm = container_of(blk, struct dm_device, blk);
>> + struct dm_target *ti;
>> + blkcnt_t tnblks, todo;
>> + sector_t tblk;
>> + int err;
>> +
>> + todo = num_blocks;
>> +
>> + list_for_each_entry(ti, &dm->targets, list) {
>> + if (block < ti->base || block >= ti->base + ti->size)
>> + continue;
>> +
>> + if (!ti->ops->write)
>> + return -EIO;
>> +
>> + tblk = block - ti->base;
>> + tnblks = min(todo, ti->size - tblk);
>> + err = ti->ops->write(ti, buf, tblk, tnblks);
>> + if (err)
>> + return err;
>> +
>> + block += tnblks;
>> + todo -= tnblks;
>> + buf += tnblks << SECTOR_SHIFT;
>> + if (!todo)
>> + return 0;
>> + }
>> +
>> + return -EIO;
>> +}
>> +
>> +static struct block_device_ops dm_blk_ops = {
>> + .read = dm_blk_read,
>> + .write = dm_blk_write,
>> +};
>> +
>> +static blkcnt_t dm_size(struct dm_device *dm)
>> +{
>> + struct dm_target *last;
>> +
>> + if (list_empty(&dm->targets))
>> + return 0;
>> +
>> + last = list_last_entry(&dm->targets, struct dm_target, list);
>> + return last->base + last->size;
>> +}
>> +
>> +static int dm_n_targets(struct dm_device *dm)
>> +{
>> + struct dm_target *ti;
>> + int n = 0;
>> +
>> + list_for_each_entry(ti, &dm->targets, list) {
>> + n++;
>> + }
>> +
>> + return n;
>> +}
>> +
>> +char *dm_asprint(struct dm_device *dm)
>> +{
>> + char **strv, *str, *tistr;
>> + struct dm_target *ti;
>> + int n = 0, strc;
>> +
>> + strc = 1 + dm_n_targets(dm);
>> + strv = xmalloc(strc * sizeof(*strv));
>
>
> Meh, maybe we should have a growable string type...
I have taken a stab at this in v2 with rasprintf(), "the realloc()ing
sprintf()". Let me know what you think.
> Anyways, we have <stringlist.h> for what you are doing here, but I leave
> it up to you whether to use it or not. Current code looks ok too.
>
>> +
>> + strv[n++] = xasprintf(
>> + "Device: %s\n"
>> + "Size: %llu\n"
>> + "Table:\n"
>> + " # Start End Size Target\n",
>> + dev_name(&dm->dev), dm_size(dm));
>> +
>> + list_for_each_entry(ti, &dm->targets, list) {
>> + tistr = ti->ops->asprint ? ti->ops->asprint(ti) : NULL;
>> +
>> + strv[n] = xasprintf("%2d %10llu %10llu %10llu %s %s\n",
>> + n - 1, ti->base, ti->base + ti->size - 1,
>> + ti->size, ti->ops->name, tistr ? : "");
>> + n++;
>> +
>> + if (tistr)
>> + free(tistr);
>> + }
>> +
>> + str = strjoin("", strv, strc);
>> +
>> + for (n = 0; n < strc; n++)
>> + free(strv[n]);
>> +
>> + free(strv);
>> +
>> + return str;
>> +}
>> +EXPORT_SYMBOL(dm_asprint);
>> +
>> +static struct dm_target *dm_parse_row(struct dm_device *dm, const char *crow)
>> +{
>> + struct dm_target *ti;
>> + char *row, **argv;
>> + int argc;
>> +
>> + row = xstrdup(crow);
>> + argc = strtokv(row, " \t", &argv);
>> +
>> + if (argc < 3) {
>> + dev_err(&dm->dev, "Invalid row: \"%s\"\n", crow);
>> + goto err;
>> + }
>> +
>> + ti = xzalloc(sizeof(*ti));
>> + ti->dm = dm;
>> +
>> + ti->ops = dm_target_ops_find(argv[2]);
>> + if (!ti->ops) {
>> + dev_err(&dm->dev, "Unknown target: \"%s\"\n", argv[2]);
>> + goto err_free;
>> + }
>> +
>> + if (kstrtoull(argv[0], 0, &ti->base)) {
>> + dm_target_err(ti, "Invalid start: \"%s\"\n", argv[0]);
>> + goto err_free;
>> + }
>> +
>> + if (ti->base != dm_size(dm)) {
>> + /* Could we just skip the start argument, then? Seems
>> + * like it, but let's keep things compatible with the
>> + * table format in Linux.
>> + */
>> + dm_target_err(ti, "Non-contiguous start: %llu, expected %llu\n",
>> + ti->base, dm_size(dm));
>> + goto err_free;
>> + }
>> +
>> + if (kstrtoull(argv[1], 0, &ti->size) || !ti->size) {
>> + dm_target_err(ti, "Invalid length: \"%s\"\n", argv[1]);
>> + goto err_free;
>> + }
>> +
>> + argc -= 3;
>> +
>> + if (ti->ops->create(ti, argc, argc ? &argv[3] : NULL))
>> + goto err_free;
>> +
>> + free(argv);
>> + free(row);
>> + return ti;
>> +
>> +err_free:
>> + free(ti);
>
> Nitpick: initialize as zero and combine the labels.
>
>> +err:
>> + free(argv);
>> + free(row);
>> + return NULL;
>> +}
>> +
>> +static int dm_parse_table(struct dm_device *dm, const char *ctable)
>> +{
>> + struct dm_target *ti, *tmp;
>> + char *table, **rowv;
>> + int i, rowc;
>> +
>> + table = xstrdup(ctable);
>> + rowc = strtokv(table, "\n", &rowv);
>> +
>> + for (i = 0; i < rowc; i++) {
>> + ti = dm_parse_row(dm, rowv[i]);
>> + if (!ti)
>> + goto err_destroy;
>> +
>> + list_add_tail(&ti->list, &dm->targets);
>> + }
>> +
>> + free(rowv);
>> + free(table);
>> + return 0;
>> +
>> +err_destroy:
>> + list_for_each_entry_safe_reverse(ti, tmp, &dm->targets, list) {
>> + ti->ops->destroy(ti);
>> + list_del(&ti->list);
>> + }
>> +
>> + free(rowv);
>> + free(table);
>> +
>> + dev_err(&dm->dev, "Failed to parse table\n");
>> + return -EINVAL;
>> +}
>> +
>> +void dm_destroy(struct dm_device *dm)
>> +{
>> + struct dm_target *ti;
>> +
>> + list_del(&dm->list);
>
> If you use a device class, this can be dropped.
>
>> +
>> + blockdevice_unregister(&dm->blk);
>> +
>> + list_for_each_entry_reverse(ti, &dm->targets, list) {
>> + ti->ops->destroy(ti);
>> + }
>> +
>> + unregister_device(&dm->dev);
>> +
>> + free(dm);
>> +}
>> +EXPORT_SYMBOL(dm_destroy);
>> +
>> +struct dm_device *dm_create(const char *name, const char *table)
>> +{
>> + struct dm_target *ti;
>> + struct dm_device *dm;
>> + int err;
>> +
>> + dm = xzalloc(sizeof(*dm));
>> +
>> + dev_set_name(&dm->dev, "%s", name);
>> + dm->dev.id = DEVICE_ID_SINGLE;
>> + err = register_device(&dm->dev);
>> + if (err)
>> + goto err_free;
>
> Add a devinfo_add(dev, dm_devinfo), which calls dm_asprint()?
> That makes it easy to inspect the virtual devices using the devinfo
> command on the shell.
Very nice, thank you!
>> +
>> + INIT_LIST_HEAD(&dm->targets);
>> + err = dm_parse_table(dm, table);
>> + if (err)
>> + goto err_unregister;
>> +
>> + dm->blk = (struct block_device) {
>> + .dev = &dm->dev,
>> + .cdev = {
>> + .name = xstrdup(name),
>> + },
>
> .cdev.name = xstrdup(name)
>
>> +
>> + .type = BLK_TYPE_VIRTUAL,
>> + .ops = &dm_blk_ops,
>> +
>> + .num_blocks = dm_size(dm),
>> + .blockbits = SECTOR_SHIFT,
>> + };
>> +
>> + err = blockdevice_register(&dm->blk);
>> + if (err)
>> + goto err_destroy;
>> +
>> + list_add_tail(&dm->list, &dm_device_list);
>> + return dm;
>> +
>> +err_destroy:
>> + list_for_each_entry_reverse(ti, &dm->targets, list) {
>> + ti->ops->destroy(ti);
>> + }
>> +
>> +err_unregister:
>> + unregister_device(&dm->dev);
>> +
>> +err_free:
>> + free(dm);
>> + return ERR_PTR(err);
>> +}
>> +EXPORT_SYMBOL(dm_create);
>> diff --git a/drivers/block/dm/dm-target.h b/drivers/block/dm/dm-target.h
>> new file mode 100644
>> index 0000000000..506e808b79
>> --- /dev/null
>> +++ b/drivers/block/dm/dm-target.h
>> @@ -0,0 +1,39 @@
>> +/* SPDX-License-Identifier: GPL-2.0-only */
>> +/* SPDX-FileCopyrightText: © 2025 Tobias Waldekranz <tobias@waldekranz.com>, Wires */
>> +
>> +#ifndef __DM_TARGET_H
>> +#define __DM_TARGET_H
>> +
>> +struct dm_device;
>> +struct dm_target_ops;
>> +
>> +struct dm_target {
>> + struct dm_device *dm;
>> + struct list_head list;
>> +
>> + sector_t base;
>> + blkcnt_t size;
>> +
>> + const struct dm_target_ops *ops;
>> + void *private;
>> +};
>> +
>> +void dm_target_err(struct dm_target *ti, const char *fmt, ...);
>> +
>> +struct dm_target_ops {
>> + struct list_head list;
>> + const char *name;
>> +
>> + char *(*asprint)(struct dm_target *ti);
>> + int (*create)(struct dm_target *ti, unsigned int argc, char **argv);
>> + int (*destroy)(struct dm_target *ti);
>> + int (*read)(struct dm_target *ti, void *buf,
>> + sector_t block, blkcnt_t num_blocks);
>> + int (*write)(struct dm_target *ti, const void *buf,
>> + sector_t block, blkcnt_t num_blocks);
>> +};
>> +
>> +int dm_target_register(struct dm_target_ops *ops);
>> +void dm_target_unregister(struct dm_target_ops *ops);
>> +
>> +#endif /* __DM_TARGET_H */
>> diff --git a/include/dm.h b/include/dm.h
>> new file mode 100644
>> index 0000000000..255796ca2f
>> --- /dev/null
>> +++ b/include/dm.h
>
> I'd prefer device-mapper.h for the header to avoid confusion with driver
> model. No need to change the symbol names though.
Reasonable, I'll rename it.
>> @@ -0,0 +1,16 @@
>> +/* SPDX-License-Identifier: GPL-2.0-only */
>> +
>> +#ifndef __DM_H
>> +#define __DM_H
>> +
>> +struct dm_device;
>> +
>> +struct dm_device *dm_find_by_name(const char *name);
>> +int dm_foreach(int (*cb)(struct dm_device *dm, void *ctx), void *ctx);
>> +
>> +char *dm_asprint(struct dm_device *dm);
>> +
>> +void dm_destroy(struct dm_device *dm);
>> +struct dm_device *dm_create(const char *name, const char *ctable);
>> +
>> +#endif /* __DM_H */
>
> --
> Pengutronix e.K. | |
> Steuerwalder Str. 21 | http://www.pengutronix.de/ |
> 31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
> Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-5555 |
next prev parent reply other threads:[~2025-09-08 10:08 UTC|newest]
Thread overview: 34+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-08-28 15:05 [PATCH 0/5] dm: Initial work on a device mapper Tobias Waldekranz
2025-08-28 15:05 ` [PATCH 1/5] string: add strtok/strtokv Tobias Waldekranz
2025-09-04 11:00 ` Ahmad Fatoum
2025-09-04 13:35 ` Tobias Waldekranz
2025-09-05 16:28 ` Ahmad Fatoum
2025-09-08 9:26 ` Tobias Waldekranz
2025-08-28 15:05 ` [PATCH 2/5] dm: Add initial device mapper infrastructure Tobias Waldekranz
2025-09-05 16:14 ` Ahmad Fatoum
2025-09-08 9:27 ` Tobias Waldekranz [this message]
2025-09-05 17:26 ` Ahmad Fatoum
2025-08-28 15:05 ` [PATCH 3/5] dm: linear: Add linear target Tobias Waldekranz
2025-08-29 5:56 ` Ahmad Fatoum
2025-09-05 16:37 ` Ahmad Fatoum
2025-08-28 15:05 ` [PATCH 4/5] test: self: dm: Add test of " Tobias Waldekranz
2025-09-05 16:50 ` Ahmad Fatoum
2025-09-08 9:27 ` Tobias Waldekranz
2025-08-28 15:05 ` [PATCH 5/5] commands: dmsetup: Basic command set for dm device management Tobias Waldekranz
2025-09-05 16:54 ` Ahmad Fatoum
2025-09-08 9:27 ` Tobias Waldekranz
2025-08-29 8:29 ` [PATCH 0/5] dm: Initial work on a device mapper Sascha Hauer
2025-08-31 7:48 ` Tobias Waldekranz
2025-09-02 8:40 ` Ahmad Fatoum
2025-09-02 9:44 ` Tobias Waldekranz
2025-08-29 11:24 ` Ahmad Fatoum
2025-08-31 7:48 ` Tobias Waldekranz
2025-09-02 9:03 ` Ahmad Fatoum
2025-09-02 13:01 ` Tobias Waldekranz
2025-09-03 7:05 ` Jan Lübbe
2025-09-02 14:46 ` Jan Lübbe
2025-09-02 21:34 ` Tobias Waldekranz
2025-09-03 6:50 ` Jan Lübbe
2025-09-03 20:19 ` Tobias Waldekranz
2025-09-05 14:44 ` Jan Lübbe
2025-09-02 14:34 ` Jan Lübbe
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=87ldmp2t1f.fsf@waldekranz.com \
--to=tobias@waldekranz.com \
--cc=a.fatoum@pengutronix.de \
--cc=barebox@lists.infradead.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