563 lines
14 KiB
C
563 lines
14 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
// SPDX-FileCopyrightText: 2008-2009 Jan Engelhardt
|
|
/*
|
|
* Bulk XFS Undelete
|
|
*
|
|
* The program extracts all inodes (with some bounding possible by
|
|
* means of specifying command line options).
|
|
*/
|
|
#ifndef _LARGEFILE_SOURCE
|
|
# define _LARGEFILE_SOURCE 1
|
|
#endif
|
|
#ifndef _LARGE_FILES
|
|
# define _LARGE_FILES 1
|
|
#endif
|
|
#ifndef _FILE_OFFSET_BITS
|
|
# define _FILE_OFFSET_BITS 64
|
|
#endif
|
|
#include <sys/ioctl.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <poll.h>
|
|
#include <signal.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <libHX/ctype_helper.h>
|
|
#include <libHX/defs.h>
|
|
#include <libHX/init.h>
|
|
#include <libHX/misc.h>
|
|
#include <libHX/option.h>
|
|
#include <libHX/proc.h>
|
|
#include <libHX/string.h>
|
|
#include <arpa/inet.h>
|
|
|
|
struct work_info {
|
|
char *device;
|
|
char *output_dir;
|
|
unsigned long long start_inode, max_inodes, truncate_threshold;
|
|
size_t size_cutoff;
|
|
unsigned int dry_run;
|
|
|
|
/* generated */
|
|
unsigned long long stop_inode, icount;
|
|
size_t blocksize;
|
|
char *buffer, *blank_buffer;
|
|
unsigned int buffer_size, inode_size;
|
|
int xdb_read, xdb_write, blkfd;
|
|
time_t stat_timestamp;
|
|
unsigned int stat_recovered;
|
|
unsigned long long stat_lastnode;
|
|
};
|
|
|
|
struct extent_entry {
|
|
unsigned long long start_offset, start_block, block_count;
|
|
};
|
|
|
|
struct extent_info {
|
|
unsigned int e_num;
|
|
struct extent_entry e_data[0];
|
|
};
|
|
|
|
struct xfs_timestamp {
|
|
uint32_t t_sec; /* timestamp seconds */
|
|
uint32_t t_nsec; /* timestamp nanoseconds */
|
|
};
|
|
|
|
struct xfs_dinode_core {
|
|
uint16_t di_magic; /* inode magic # = XFS_DINODE_MAGIC */
|
|
uint16_t di_mode; /* mode and type of file */
|
|
uint8_t di_version; /* inode version */
|
|
uint8_t di_format; /* format of di_c data */
|
|
uint16_t di_onlink; /* old number of links to file */
|
|
uint32_t di_uid; /* owner's user id */
|
|
uint32_t di_gid; /* owner's group id */
|
|
uint32_t di_nlink; /* number of links to file */
|
|
uint16_t di_projid; /* owner's project id */
|
|
uint8_t di_pad[8]; /* unused, zeroed space */
|
|
uint16_t di_flushiter; /* incremented on flush */
|
|
struct xfs_timestamp di_atime; /* time last accessed */
|
|
struct xfs_timestamp di_mtime; /* time last modified */
|
|
struct xfs_timestamp di_ctime; /* time created/inode modified */
|
|
uint32_t di_size; /* number of bytes in file */
|
|
uint32_t di_nblocks; /* # of direct & btree blocks used */
|
|
uint32_t di_extsize; /* basic/minimum extent size for file */
|
|
uint32_t di_nextents; /* number of extents in data fork */
|
|
uint16_t di_anextents; /* number of extents in attribute fork*/
|
|
uint8_t di_forkoff; /* attr fork offs, <<3 for 64b align */
|
|
int8_t di_aformat; /* format of attr fork's data */
|
|
uint32_t di_dmevmask; /* DMIG event mask */
|
|
uint16_t di_dmstate; /* DMIG state info */
|
|
uint16_t di_flags; /* random flags, XFS_DIFLAG_... */
|
|
uint32_t di_gen; /* generation number */
|
|
};
|
|
|
|
static inline void ir_fdwait(const struct work_info *wi)
|
|
{
|
|
struct pollfd poll_data = {
|
|
.fd = wi->xdb_read,
|
|
.events = POLLIN,
|
|
};
|
|
|
|
poll(&poll_data, 1, -1);
|
|
}
|
|
|
|
static char *ir_command(const struct work_info *wi, const char *cmd)
|
|
{
|
|
ssize_t ret, have_read = 0;
|
|
int amount;
|
|
|
|
if (strchr(cmd, '\n') == NULL) {
|
|
fprintf(stderr, "Command must contain a newline: \"%s\"\n", cmd);
|
|
abort();
|
|
}
|
|
write(wi->xdb_write, cmd, strlen(cmd));
|
|
|
|
*wi->buffer = '\0';
|
|
do {
|
|
ir_fdwait(wi);
|
|
ioctl(wi->xdb_read, FIONREAD, &amount);
|
|
ret = read(wi->xdb_read, wi->buffer + have_read, amount);
|
|
if (ret < 0) {
|
|
perror("read");
|
|
abort();
|
|
} else if (ret == 0) {
|
|
continue;
|
|
}
|
|
have_read += ret;
|
|
wi->buffer[have_read] = '\0';
|
|
if (strstr(wi->buffer, "xfs_db> ") != NULL)
|
|
break;
|
|
} while (true);
|
|
|
|
return wi->buffer;
|
|
}
|
|
|
|
static inline uint64_t ir_ntoh64(uint64_t x)
|
|
{
|
|
#ifdef WORDS_BIGENDIAN
|
|
return x;
|
|
#else
|
|
uint32_t lo = x, hi = x >> 32;
|
|
return (static_cast(uint64_t, htonl(lo)) << 32) | htonl(hi);
|
|
#endif
|
|
}
|
|
|
|
static void ir_read_dinode(const struct work_info *wi, size_t pos,
|
|
struct xfs_dinode_core *i)
|
|
{
|
|
lseek(wi->blkfd, pos, SEEK_SET);
|
|
read(wi->blkfd, i, sizeof(*i));
|
|
i->di_magic = ntohs(i->di_magic);
|
|
i->di_mode = ntohs(i->di_mode);
|
|
i->di_size = ir_ntoh64(i->di_size);
|
|
}
|
|
|
|
static struct extent_info *ir_extent_list2(char **data, int num)
|
|
{
|
|
struct extent_info *ext_list;
|
|
struct extent_entry *ext;
|
|
unsigned int ext_count = 0;
|
|
char *w;
|
|
int i;
|
|
|
|
ext_list = malloc(sizeof(struct extent_info) +
|
|
sizeof(struct extent_entry) * num);
|
|
if (ext_list == NULL) {
|
|
perror("malloc");
|
|
abort();
|
|
}
|
|
|
|
ext_list->e_num = num;
|
|
for (i = 0; i < num; ++i) {
|
|
/* 0:[0,54525968,2077652,0] */
|
|
w = data[i];
|
|
if (!HX_isdigit(*w))
|
|
continue;
|
|
while (HX_isdigit(*w))
|
|
++w;
|
|
if (*w++ != ':')
|
|
continue;
|
|
if (*w++ != '[') /* ] */
|
|
continue;
|
|
|
|
ext = &ext_list->e_data[ext_count];
|
|
ext->start_offset = strtoll(w, &w, 0);
|
|
if (*w++ != ',')
|
|
continue;
|
|
ext->start_block = strtoll(w, &w, 0);
|
|
if (*w++ != ',')
|
|
continue;
|
|
ext->block_count = strtoll(w, &w, 0);
|
|
if (*w != ',')
|
|
continue;
|
|
++ext_count;
|
|
}
|
|
|
|
memset(&ext_list[ext_count], 0, sizeof(*ext_list));
|
|
HX_zvecfree(data);
|
|
return ext_list;
|
|
}
|
|
|
|
static struct extent_info *ir_extent_list(struct work_info *wi,
|
|
unsigned long long inum)
|
|
{
|
|
char buf[64], *ret, *wp, **lines, **exts;
|
|
int num = 0, i;
|
|
|
|
snprintf(buf, sizeof(buf), "inode %llu\n", inum);
|
|
ir_command(wi, buf);
|
|
ir_command(wi, "type inode\n");
|
|
ret = ir_command(wi, "print\n");
|
|
lines = HX_split(ret, "\n", &num, 0);
|
|
|
|
for (i = 0; i < num; ++i)
|
|
if (strncmp(lines[i], "u.bmx[", strlen("u.bmx[")) == 0) /* ]] */
|
|
break;
|
|
|
|
if (i == num)
|
|
/* no extents */
|
|
return NULL;
|
|
|
|
wp = lines[i];
|
|
while (!HX_isspace(*wp) && *wp != '\0')
|
|
++wp;
|
|
while (HX_isspace(*wp))
|
|
++wp;
|
|
if (*wp++ != '=')
|
|
return NULL;
|
|
while (HX_isspace(*wp))
|
|
++wp;
|
|
if (*wp++ != '[') /* ] */
|
|
return NULL;
|
|
while (*wp != ']')
|
|
if (*wp++ == '\0')
|
|
return NULL;
|
|
++wp;
|
|
while (HX_isspace(*wp))
|
|
++wp;
|
|
|
|
num = 0;
|
|
exts = HX_split(wp, " ", &num, 0);
|
|
HX_zvecfree(lines);
|
|
return ir_extent_list2(exts, num);
|
|
}
|
|
|
|
static void ir_xfer(int ofd, int ifd, size_t z, const struct work_info *wi)
|
|
{
|
|
unsigned int segment;
|
|
ssize_t ret;
|
|
|
|
while (z > 0) {
|
|
segment = (z < wi->buffer_size) ? z : wi->buffer_size;
|
|
ret = read(ifd, wi->buffer, segment);
|
|
if (ret < 0) {
|
|
perror("read");
|
|
break;
|
|
} else if (ret == 0) {
|
|
break;
|
|
}
|
|
if (memcmp(wi->buffer, wi->blank_buffer, segment) != 0)
|
|
write(ofd, wi->buffer, ret);
|
|
else
|
|
lseek(ofd, ret, SEEK_CUR);
|
|
z -= ret;
|
|
}
|
|
}
|
|
|
|
static void ir_copy(const struct work_info *wi, unsigned long long inum,
|
|
const struct xfs_dinode_core *inode, const struct extent_info *ext_info)
|
|
{
|
|
const struct extent_entry *ext;
|
|
struct stat sb;
|
|
unsigned int i;
|
|
char buf[256];
|
|
int fd;
|
|
size_t rem, qty;
|
|
|
|
snprintf(buf, sizeof(buf), "%s/%llu", wi->output_dir, inum);
|
|
if ((fd = open(buf, O_WRONLY | O_CREAT | O_TRUNC,
|
|
inode->di_mode)) < 0) {
|
|
fprintf(stderr, "open: %s: %s\n", buf, strerror(errno));
|
|
return;
|
|
}
|
|
|
|
rem = inode->di_size;
|
|
for (i = 0; i < ext_info->e_num && rem > 0; ++i) {
|
|
ext = &ext_info->e_data[i];
|
|
lseek(wi->blkfd, wi->blocksize * ext->start_block, SEEK_SET);
|
|
lseek(fd, wi->blocksize * ext->start_offset, SEEK_SET);
|
|
qty = wi->blocksize * ext->block_count;
|
|
if (qty > rem)
|
|
qty = rem;
|
|
ir_xfer(fd, wi->blkfd, qty, wi);
|
|
rem -= qty;
|
|
}
|
|
|
|
if (fstat(fd, &sb) == 0 && (sb.st_size > wi->truncate_threshold ||
|
|
inode->di_size > wi->truncate_threshold) &&
|
|
inode->di_size < sb.st_size)
|
|
ftruncate(fd, inode->di_size);
|
|
close(fd);
|
|
}
|
|
|
|
static inline void ir_stats(unsigned long long inum, struct work_info *wi)
|
|
{
|
|
time_t now = time(NULL);
|
|
unsigned long long delta_ino, rem_ino, rem_part;
|
|
unsigned int rem_sec, rem_min, rem_hour;
|
|
|
|
if (now <= wi->stat_timestamp + 3)
|
|
return;
|
|
|
|
delta_ino = inum - wi->stat_lastnode;
|
|
if (delta_ino < 256)
|
|
return;
|
|
rem_ino = wi->stop_inode - inum;
|
|
rem_part = rem_ino / delta_ino;
|
|
rem_sec = rem_part % 60;
|
|
rem_min = rem_part / 60 % 60;
|
|
rem_hour = rem_part / 3600;
|
|
|
|
printf("\rino %llu/%llu (%.2f%%) %llu/s ETA %uh:%02um:%02us recov %u\e[K", /* ] */
|
|
inum, wi->stop_inode,
|
|
static_cast(double, inum * 100) / wi->stop_inode,
|
|
delta_ino, rem_hour, rem_min, rem_sec, wi->stat_recovered);
|
|
wi->stat_lastnode = inum;
|
|
wi->stat_timestamp = now;
|
|
}
|
|
|
|
static void ir_extract(struct work_info *wi)
|
|
{
|
|
unsigned long long inum;
|
|
struct xfs_dinode_core inode;
|
|
struct extent_info *ext_list;
|
|
size_t ino_pos;
|
|
|
|
for (inum = wi->start_inode, ino_pos = inum * wi->inode_size;
|
|
inum < wi->stop_inode; ++inum, ino_pos += wi->inode_size)
|
|
{
|
|
ir_stats(inum, wi);
|
|
ir_read_dinode(wi, ino_pos, &inode);
|
|
if (inode.di_magic != 0x494E || inode.di_size == 0 ||
|
|
!S_ISREG(inode.di_mode)) {
|
|
fflush(stdout);
|
|
continue;
|
|
}
|
|
|
|
/* Skip extentless files before even trying to ask. */
|
|
ext_list = ir_extent_list(wi, inum);
|
|
if (ext_list == NULL) {
|
|
fflush(stdout);
|
|
continue;
|
|
}
|
|
|
|
if (inode.di_size >= wi->size_cutoff) {
|
|
printf("\n" "ino %llu is pretty large (size %Zu MB), "
|
|
"skipping.\n", inum,
|
|
static_cast(size_t, inode.di_size) >> 20);
|
|
} else if (!wi->dry_run) {
|
|
ir_copy(wi, inum, &inode, ext_list);
|
|
++wi->stat_recovered;
|
|
}
|
|
|
|
free(ext_list);
|
|
}
|
|
}
|
|
|
|
static struct HXproc xdb_proc = {
|
|
.p_flags = HXPROC_VERBOSE | HXPROC_STDIN | HXPROC_STDOUT,
|
|
};
|
|
|
|
static bool ir_get_devinfo(struct work_info *work_info)
|
|
{
|
|
const char *const args[] = {"xfs_db", "-r", work_info->device, NULL};
|
|
unsigned long long di_dblocks = 0, di_blocksize = 0, di_inodesize = 0;
|
|
int num_lines = 0, i;
|
|
char *ret, **lines;
|
|
|
|
if ((i = HXproc_run_async(args, &xdb_proc)) <= 0) {
|
|
fprintf(stderr, "Error starting xfs_db: %s\n", strerror(-i));
|
|
return false;
|
|
}
|
|
|
|
work_info->xdb_read = xdb_proc.p_stdout;
|
|
work_info->xdb_write = xdb_proc.p_stdin;
|
|
/* munge prompt */
|
|
ir_fdwait(work_info);
|
|
i = read(work_info->xdb_read, work_info->buffer,
|
|
strlen("xfs_db> "));
|
|
if (i != strlen("xfs_db> ")) {
|
|
fprintf(stderr, "Crap: %s\n", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
ir_command(work_info, "inode 0\n");
|
|
ir_command(work_info, "type sb\n");
|
|
ret = ir_command(work_info, "print\n");
|
|
lines = HX_split(ret, "\n", &num_lines, 0);
|
|
if (lines == NULL) {
|
|
fprintf(stderr, "HX_split: %s\n", strerror(errno));
|
|
return false;
|
|
}
|
|
for (i = 0; i < num_lines; ++i) {
|
|
const char *key = lines[i];
|
|
char *value = lines[i];
|
|
|
|
while (!HX_isspace(*value) && *value != '\0')
|
|
++value;
|
|
*value++ = '\0';
|
|
while (HX_isspace(*value))
|
|
++value;
|
|
if (*value++ != '=')
|
|
continue;
|
|
while (HX_isspace(*value))
|
|
++value;
|
|
|
|
if (strcmp(key, "dblocks") == 0)
|
|
di_dblocks = strtoull(value, NULL, 0);
|
|
else if (strcmp(key, "blocksize") == 0)
|
|
di_blocksize = strtoull(value, NULL, 0);
|
|
else if (strcmp(key, "inodesize") == 0)
|
|
di_inodesize = strtoull(value, NULL, 0);
|
|
}
|
|
HX_zvecfree(lines);
|
|
|
|
if (di_dblocks == 0 || di_blocksize == 0 || di_inodesize == 0) {
|
|
fprintf(stderr, "Insufficient SB data\n");
|
|
return false;
|
|
}
|
|
|
|
work_info->blocksize = di_blocksize;
|
|
work_info->inode_size = di_inodesize;
|
|
work_info->icount = di_dblocks * di_blocksize / di_inodesize;
|
|
return true;
|
|
}
|
|
|
|
static bool ir_get_options(int *argc, const char ***argv,
|
|
struct work_info *work_info)
|
|
{
|
|
struct stat sb;
|
|
struct HXoption options_table[] = {
|
|
{.sh = 'D', .type = HXTYPE_STRING, .ptr = &work_info->device,
|
|
.help = "Device name", .htyp = "dev"},
|
|
{.sh = 'N', .type = HXTYPE_ULLONG,
|
|
.ptr = &work_info->max_inodes,
|
|
.help = "Maximum number of inodes to scan"},
|
|
{.sh = 'n', .type = HXTYPE_NONE, .ptr = &work_info->dry_run,
|
|
.help = "Dry run"},
|
|
{.sh = 'o', .type = HXTYPE_STRING,
|
|
.ptr = &work_info->output_dir, .htyp = "dir",
|
|
.help = "Output directory"},
|
|
{.sh = 'r', .type = HXTYPE_ULLONG,
|
|
.ptr = &work_info->start_inode,
|
|
.help = "Start at the specified inode"},
|
|
{.sh = 's', .type = HXTYPE_ULLONG,
|
|
.ptr = &work_info->size_cutoff, .htyp = "size",
|
|
.help = "Do not extract files larger than size without asking"},
|
|
{.sh = 't', .type = HXTYPE_ULLONG,
|
|
.ptr = &work_info->truncate_threshold, .htyp = "size",
|
|
.help = "Files less than the size are not subject to truncation"},
|
|
HXOPT_AUTOHELP,
|
|
HXOPT_TABLEEND,
|
|
};
|
|
if (HX_getopt(options_table, argc, argv, HXOPT_USAGEONERR) !=
|
|
HXOPT_ERR_SUCCESS)
|
|
return false;
|
|
|
|
if (work_info->device == NULL) {
|
|
fprintf(stderr, "%s: -D option is required\n",
|
|
HX_basename(**argv));
|
|
return false;
|
|
}
|
|
if (work_info->output_dir == NULL) {
|
|
fprintf(stderr, "%s: -o option is required\n",
|
|
HX_basename(**argv));
|
|
return false;
|
|
}
|
|
|
|
if (stat(work_info->output_dir, &sb) < 0 ||
|
|
!S_ISDIR(sb.st_mode)) {
|
|
fprintf(stderr, "Output dir non-existant or something wrong.\n");
|
|
return false;
|
|
}
|
|
|
|
work_info->buffer_size = 1024 << 10;
|
|
if ((work_info->buffer = malloc(work_info->buffer_size)) == NULL) {
|
|
fprintf(stderr, "malloc: %s\n", strerror(errno));
|
|
return false;
|
|
}
|
|
if ((work_info->blank_buffer =
|
|
calloc(1, work_info->buffer_size)) == NULL) {
|
|
fprintf(stderr, "calloc: %s\n", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
work_info->blkfd = open(work_info->device, O_RDONLY);
|
|
if (work_info->blkfd < 0) {
|
|
fprintf(stderr, "open: %s: %s\n",
|
|
work_info->device, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
if (!ir_get_devinfo(work_info))
|
|
return false;
|
|
|
|
fprintf(stderr, "Filesystem claims to have %llu inodes\n", work_info->icount);
|
|
if (work_info->max_inodes == 0 ||
|
|
work_info->max_inodes > work_info->icount)
|
|
work_info->max_inodes = work_info->icount;
|
|
work_info->stop_inode = work_info->start_inode + work_info->max_inodes;
|
|
if (work_info->stop_inode > work_info->icount) {
|
|
work_info->stop_inode = work_info->icount;
|
|
work_info->max_inodes = work_info->stop_inode - work_info->start_inode;
|
|
}
|
|
fprintf(stderr, "Inode range %llu--%llu\n", work_info->start_inode,
|
|
work_info->stop_inode);
|
|
return true;
|
|
}
|
|
|
|
static int main2(int argc, const char **argv)
|
|
{
|
|
struct work_info work_info;
|
|
|
|
memset(&work_info, 0, sizeof(work_info));
|
|
work_info.size_cutoff = 1024 << 20; /* 1 GB */
|
|
|
|
if (!ir_get_options(&argc, &argv, &work_info))
|
|
return EXIT_FAILURE;
|
|
|
|
ir_extract(&work_info);
|
|
|
|
close(work_info.blkfd);
|
|
close(work_info.xdb_write);
|
|
close(work_info.xdb_read);
|
|
free(work_info.buffer);
|
|
free(work_info.blank_buffer);
|
|
free(work_info.device);
|
|
free(work_info.output_dir);
|
|
kill(xdb_proc.p_pid, SIGTERM); /* just in case */
|
|
HXproc_wait(&xdb_proc);
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
int main(int argc, const char **argv)
|
|
{
|
|
int ret;
|
|
|
|
if ((ret = HX_init()) < 0) {
|
|
fprintf(stderr, "HX_init: %s\n", strerror(errno));
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
ret = main2(argc, argv);
|
|
HX_exit();
|
|
return ret;
|
|
}
|