summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKent Overstreet <kent.overstreet@linux.dev>2025-09-01 14:53:13 -0400
committerKent Overstreet <kent.overstreet@linux.dev>2025-09-01 15:11:56 -0400
commite65dd86e419d96aaa616dcb508d3564bd5721ef6 (patch)
treeb307ec60ce65af9a10c8f65b4b7e8a23b55fbffc
parentda8f1d04e3b89a576cbb5fdd7cd1c1c451be11d8 (diff)
bcachefs undump
Add our own version of 'qemu-img convert', which doesn't have the l1 table size limit or require fixing our missing reflink table. Signed-off-by: Kent Overstreet <kent.overstreet@linux.dev>
-rw-r--r--c_src/bcachefs.c1
-rw-r--r--c_src/cmd_dump.c82
-rw-r--r--c_src/cmds.h2
-rw-r--r--c_src/qcow2.c51
-rw-r--r--c_src/qcow2.h4
-rw-r--r--c_src/tools-util.c6
-rw-r--r--c_src/tools-util.h7
-rw-r--r--src/bcachefs.rs1
8 files changed, 148 insertions, 6 deletions
diff --git a/c_src/bcachefs.c b/c_src/bcachefs.c
index 58fa2549..d91e0475 100644
--- a/c_src/bcachefs.c
+++ b/c_src/bcachefs.c
@@ -96,6 +96,7 @@ void bcachefs_usage(void)
"Debug:\n"
"These commands work on offline, unmounted filesystems\n"
" dump Dump filesystem metadata to a qcow2 image\n"
+ " undump Convert qcow2 metadata dumps to sparse raw files\n"
" list List filesystem metadata in textual form\n"
" list_journal List contents of journal\n"
"\n"
diff --git a/c_src/cmd_dump.c b/c_src/cmd_dump.c
index 5f354272..93e7d743 100644
--- a/c_src/cmd_dump.c
+++ b/c_src/cmd_dump.c
@@ -386,3 +386,85 @@ int cmd_dump(int argc, char *argv[])
darray_exit(&dev_names);
return ret;
}
+
+static void undump_usage(void)
+{
+ puts("bcachefs undump - turn qcow2 images from 'bcachefs dump' back into sparse raw images\n"
+ "Usage: bcachefs undump [OPTION]... <files>\n"
+ "\n"
+ "Options:\n"
+ " -f, --force Force; overwrite when needed\n"
+ " -h, --help Display this help and exit\n"
+ "Report bugs to <linux-bcachefs@vger.kernel.org>");
+}
+
+struct undump {
+ char *in, *out;
+ int infd, outfd;
+};
+
+int cmd_undump(int argc, char *argv[])
+{
+ static const struct option longopts[] = {
+ { "force", no_argument, NULL, 'f' },
+ { "help", no_argument, NULL, 'h' },
+ { NULL }
+ };
+ bool force = false;
+ int opt;
+
+ while ((opt = getopt_long(argc, argv, "fh",
+ longopts, NULL)) != -1)
+ switch (opt) {
+ case 'f':
+ force = true;
+ break;
+ case 'h':
+ undump_usage();
+ exit(EXIT_SUCCESS);
+ }
+ args_shift(optind);
+
+ if (!argc) {
+ undump_usage();
+ die("Please supply file(s) to convert");
+ }
+
+ DARRAY(struct undump) files = {};
+
+ for (unsigned i = 0; i < argc; i++) {
+ unsigned len = strlen(argv[i]);
+ const char *suffix = ".qcow2";
+ unsigned suffixlen = strlen(suffix);
+
+ if (len <= suffixlen ||
+ strcmp(suffix, argv[i] + len - suffixlen)) {
+ die("%s not a qcow2 image?", argv[i]);
+ }
+
+ char *out = strdup(argv[i]);
+ out[len - suffixlen] = '\0';
+
+ if (!force && !access(out, F_OK))
+ die("%s already exists", out);
+
+ darray_push(&files, ((struct undump) {
+ .in = argv[i],
+ .out = out,
+ .infd = xopen(argv[i], O_RDONLY),
+ }));
+ }
+
+ darray_for_each(files, i)
+ i->outfd = xopen(i->out, O_WRONLY|O_CREAT|(!force ? O_EXCL : 0), 0600);
+
+ darray_for_each(files, i) {
+ qcow2_to_raw(i->infd, i->outfd);
+ close(i->infd);
+ close(i->outfd);
+ free(i->out);
+ }
+
+ darray_exit(&files);
+ return 0;
+}
diff --git a/c_src/cmds.h b/c_src/cmds.h
index 30b47286..fa4b87a9 100644
--- a/c_src/cmds.h
+++ b/c_src/cmds.h
@@ -34,6 +34,8 @@ int cmd_fsck(int argc, char *argv[]);
int cmd_recovery_pass(int argc, char *argv[]);
int cmd_dump(int argc, char *argv[]);
+int cmd_undump(int argc, char *argv[]);
+
int cmd_list_journal(int argc, char *argv[]);
int cmd_kill_btree_node(int argc, char *argv[]);
diff --git a/c_src/qcow2.c b/c_src/qcow2.c
index 53959a00..7afb6812 100644
--- a/c_src/qcow2.c
+++ b/c_src/qcow2.c
@@ -143,8 +143,7 @@ void qcow2_image_finish(struct qcow2_image *img)
memset(buf, 0, img->block_size);
memcpy(buf, &hdr, sizeof(hdr));
- xpwrite(img->outfd, buf, img->block_size, 0,
- "qcow2 header");
+ xpwrite(img->outfd, buf, img->block_size, 0, "qcow2 header");
free(img->l2_table);
free(img->l1_table);
@@ -160,3 +159,51 @@ void qcow2_write_image(int infd, int outfd, ranges *data,
qcow2_write_ranges(&img, data);
qcow2_image_finish(&img);
}
+
+void qcow2_to_raw(int infd, int outfd)
+{
+ struct qcow2_hdr hdr;
+
+ xpread(infd, &hdr, sizeof(hdr), 0);
+
+ if (hdr.magic != cpu_to_be32(QCOW_MAGIC))
+ die("not a qcow2 image");
+
+ if (hdr.version != cpu_to_be32(QCOW_VERSION))
+ die("incorrect qcow2 version");
+
+ ftruncate(outfd, be64_to_cpu(hdr.size));
+
+ unsigned block_size = 1U << be32_to_cpu(hdr.block_bits);
+
+ unsigned l1_size = be32_to_cpu(hdr.l1_size);
+ unsigned l2_size = block_size / sizeof(u64);
+
+ __be64 *l1_table = xcalloc(l1_size, sizeof(u64));
+ __be64 *l2_table = xmalloc(block_size);
+ void *data_buf = xmalloc(block_size);
+
+ xpread(infd, l1_table, l1_size * sizeof(u64), be64_to_cpu(hdr.l1_table_offset));
+
+ for (u64 i = 0; i < l1_size; i++) {
+ if (!l1_table[i])
+ continue;
+
+ xpread(infd, l2_table, block_size, be64_to_cpu(l1_table[i]) & ~QCOW_OFLAG_COPIED);
+
+ for (unsigned j = 0; j < l2_size; j++) {
+ u64 src_offset = be64_to_cpu(l2_table[j]) & ~QCOW_OFLAG_COPIED;
+ if (!src_offset)
+ continue;
+
+ u64 dst_offset = (i * l2_size + j) * block_size;
+
+ xpread(infd, data_buf, block_size, src_offset);
+ xpwrite(outfd, data_buf, block_size, dst_offset, "qcow2 data");
+ }
+ }
+
+ free(data_buf);
+ free(l2_table);
+ free(l1_table);
+}
diff --git a/c_src/qcow2.h b/c_src/qcow2.h
index c7b35627..7ccb2773 100644
--- a/c_src/qcow2.h
+++ b/c_src/qcow2.h
@@ -4,6 +4,8 @@
#include <linux/types.h>
#include "tools-util.h"
+#define QCOW2_L1_MAX (4ULL << 20)
+
struct qcow2_image {
int infd;
int outfd;
@@ -25,4 +27,6 @@ void qcow2_image_finish(struct qcow2_image *);
void qcow2_write_image(int, int, ranges *, unsigned);
+void qcow2_to_raw(int, int);
+
#endif /* _QCOW2_H */
diff --git a/c_src/tools-util.c b/c_src/tools-util.c
index f48d3f21..7733b9e9 100644
--- a/c_src/tools-util.c
+++ b/c_src/tools-util.c
@@ -52,15 +52,15 @@ char *mprintf(const char *fmt, ...)
return str;
}
-void xpread(int fd, void *buf, size_t count, off_t offset)
+void __xpread(int fd, void *buf, size_t count, off_t offset, const char *file, unsigned line)
{
while (count) {
ssize_t r = pread(fd, buf, count, offset);
if (r < 0)
- die("read error: %m");
+ die("read error: %m at %s:%u", file, line);
if (!r)
- die("pread error: unexpected eof");
+ die("pread error: unexpected eof at %s:%u", file, line);
count -= r;
offset += r;
}
diff --git a/c_src/tools-util.h b/c_src/tools-util.h
index b8104002..988e2d62 100644
--- a/c_src/tools-util.h
+++ b/c_src/tools-util.h
@@ -28,8 +28,13 @@ void die(const char *, ...)
__attribute__ ((format (printf, 1, 2))) noreturn;
char *mprintf(const char *, ...)
__attribute__ ((format (printf, 1, 2)));
-void xpread(int, void *, size_t, off_t);
+
+void __xpread(int, void *, size_t, off_t, const char *, unsigned);
+#define xpread(_fd, _buf, _count, _offset) \
+ __xpread(_fd, _buf, _count, _offset, __FILE__, __LINE__)
+
void xpwrite(int, const void *, size_t, off_t, const char *);
+
struct stat xfstatat(int, const char *, int);
struct stat xfstat(int);
struct stat xstat(const char *);
diff --git a/src/bcachefs.rs b/src/bcachefs.rs
index 4fb0b14c..d0c1d6e8 100644
--- a/src/bcachefs.rs
+++ b/src/bcachefs.rs
@@ -48,6 +48,7 @@ fn handle_c_command(mut argv: Vec<String>, symlink_cmd: Option<&str>) -> i32 {
"data" => c::data_cmds(argc, argv),
"device" => c::device_cmds(argc, argv),
"dump" => c::cmd_dump(argc, argv),
+ "undump" => c::cmd_undump(argc, argv),
"format" => c::cmd_format(argc, argv),
"fs" => c::fs_cmds(argc, argv),
"fsck" => c::cmd_fsck(argc, argv),