947 lines
31 KiB
C
947 lines
31 KiB
C
/**
|
|
* A tool for ext2/ext3/ext4 filesystems that allows to change inode count
|
|
* without recreating the FS.
|
|
*
|
|
* Copyright (c) Vitaliy Filippov <vitalif@mail.ru> 2013+
|
|
* License: GNU GPLv2 or later
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
/**
|
|
* TODO try to not fragment the journal while moving
|
|
*
|
|
* The theory isn't that hard:
|
|
* 1) Determine where we want to move the inode tables:
|
|
* 1.1) Create a map of non-movable blocks - these are: superblock & group descriptors,
|
|
* block bitmaps, inode bitmaps, bad blocks, and blocks in the resize inode.
|
|
* Otherwise speaking, we can move any block that either belongs to inode table
|
|
* or belongs to any inode other than the resize or bad blocks inode.
|
|
* 1.2) Using the created map, find place for each group inode table closest to the beginning
|
|
* of its flex_bg. Save these locations in memory.
|
|
* 1.3) Free the map.
|
|
* 2) Only if shrinking: move inodes away from the end of each block group inode table
|
|
* 2.1) move each inode to the new place, mark new place as occupied, unmark old one
|
|
* 2.2) remember the old->new inode number mapping
|
|
* 3) Move data away from extra blocks needed by growing/moved inode tables:
|
|
* 3.2) Create a map of blocks that we want to free.
|
|
* 3.3) Iterate through all inodes and move blocks. It may involve overwriting
|
|
* the whole file extent tree or block mapping.
|
|
* 4) Change all inode numbers in directory entries according to mappings from (1.2),
|
|
* and then using a formula: new_num = 1 + ((old_num-1)/old_i_per_g)*new_i_per_g + ((old_num-1) % old_i_per_g)
|
|
* 5) Move inode tables.
|
|
* 6) Unmark old inode table blocks, mark new ones.
|
|
* 7) Change block group descriptors: bg_inode_table, bg_free_inodes_count,
|
|
* bg_free_blocks_count, bg_inode_bitmap_csum, bg_itable_unused
|
|
* 8) Change superblock: s_inodes_count, s_free_blocks_count,
|
|
* s_free_inodes_count, s_inodes_per_group
|
|
*
|
|
* This is a highly destructive process and WILL leave a corrupted FS if interrupted.
|
|
* So we should provide a rollback method. undo_io_manager is very slow, so instead of it
|
|
* we use the "patch" i/o manager that first does not modify the filesystem directly, but
|
|
* only writes changed blocks to a separate sparse "patch" file.
|
|
*
|
|
* The modifications can then be applied to the real filesystem using e2patch utility,
|
|
* and if that process gets interrupted you can safely restart it and still get a consistent
|
|
* filesystem state.
|
|
*
|
|
* Moreover, you can create an "undo" ("backup") patch before applying modifications,
|
|
* also using e2patch utility.
|
|
*/
|
|
|
|
/**
|
|
* Need tests for:
|
|
* - both ext2 and ext4 filesystem images
|
|
* - inode moving: FS image with many files
|
|
* - block moving, including extent blocks: big sparse file with many extents
|
|
* - block moving between different groups
|
|
* - reallocation (both shrink and grow) in case of big flex_bg when inode
|
|
* tables don't fit into single block group
|
|
* - non-optimal reallocation, i.e. when inode block count doesn't change
|
|
* - reallocation on a bigalloc filesystem
|
|
*/
|
|
|
|
#define _LARGEFILE_SOURCE
|
|
#define _LARGEFILE64_SOURCE
|
|
|
|
#include <stdio.h>
|
|
#include <fcntl.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <stdlib.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/time.h>
|
|
#include <errno.h>
|
|
|
|
#include <ext2fs/ext2_fs.h>
|
|
#include <ext2fs/ext2fs.h>
|
|
|
|
#include "bmove.h"
|
|
#include "check_uninit.h"
|
|
#include "patch_io.h"
|
|
|
|
#define _(a) (a)
|
|
#define min(a,b) ((a)<(b)?(a):(b))
|
|
|
|
// Check our own feature sets for the case of linking with newer libext2fs
|
|
#define REALLOC_FEATURE_COMPAT_SUPP \
|
|
(EXT2_FEATURE_COMPAT_DIR_PREALLOC|\
|
|
EXT2_FEATURE_COMPAT_IMAGIC_INODES|\
|
|
EXT3_FEATURE_COMPAT_HAS_JOURNAL|\
|
|
EXT2_FEATURE_COMPAT_RESIZE_INODE|\
|
|
EXT2_FEATURE_COMPAT_DIR_INDEX|\
|
|
EXT2_FEATURE_COMPAT_EXT_ATTR)
|
|
// 'journal_dev' and 'recover' incompat features are unsupported
|
|
#define REALLOC_FEATURE_INCOMPAT_SUPP \
|
|
(EXT2_FEATURE_INCOMPAT_FILETYPE|\
|
|
EXT2_FEATURE_INCOMPAT_COMPRESSION|\
|
|
EXT2_FEATURE_INCOMPAT_META_BG|\
|
|
EXT3_FEATURE_INCOMPAT_EXTENTS|\
|
|
EXT4_FEATURE_INCOMPAT_FLEX_BG|\
|
|
EXT4_FEATURE_INCOMPAT_MMP|\
|
|
EXT4_FEATURE_INCOMPAT_64BIT)
|
|
#define REALLOC_FEATURE_RO_COMPAT_SUPP \
|
|
(EXT2_FEATURE_RO_COMPAT_SPARSE_SUPER|\
|
|
EXT4_FEATURE_RO_COMPAT_HUGE_FILE|\
|
|
EXT2_FEATURE_RO_COMPAT_LARGE_FILE|\
|
|
EXT4_FEATURE_RO_COMPAT_DIR_NLINK|\
|
|
EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE|\
|
|
EXT4_FEATURE_RO_COMPAT_METADATA_CSUM|\
|
|
EXT4_FEATURE_RO_COMPAT_GDT_CSUM|\
|
|
EXT4_FEATURE_RO_COMPAT_BIGALLOC|\
|
|
EXT4_FEATURE_RO_COMPAT_QUOTA)
|
|
|
|
// "local data" for the inode reallocation process
|
|
typedef struct
|
|
{
|
|
ext2_filsys fs;
|
|
int fs_fd;
|
|
dgrp_t flexbg_size, flex_count;
|
|
char *device_name, *io_options, *patch_file;
|
|
__u32 ig_old, ig_new; // old and new inodes-per-group count
|
|
__u32 ibg_old, ibg_new; // old and new inode_blocks-per-group count
|
|
__u32 new_inode_count;
|
|
blk64_t *new_itable_loc;
|
|
// (old->new) inode number map
|
|
ext2_ino_t *inode_map;
|
|
__u32 inode_map_size, inode_map_alloc;
|
|
} realloc_data;
|
|
|
|
// Utility functions for (old -> new) inode number map
|
|
void realloc_add_inode_map(realloc_data *rd, ext2_ino_t old, ext2_ino_t new)
|
|
{
|
|
if (!old || !new)
|
|
{
|
|
return;
|
|
}
|
|
if (2*rd->inode_map_size >= rd->inode_map_alloc)
|
|
{
|
|
rd->inode_map_alloc += 1024;
|
|
rd->inode_map = realloc(rd->inode_map, sizeof(ext2_ino_t) * rd->inode_map_alloc);
|
|
}
|
|
rd->inode_map[rd->inode_map_size*2] = old;
|
|
rd->inode_map[rd->inode_map_size*2+1] = new;
|
|
rd->inode_map_size++;
|
|
}
|
|
|
|
int realloc_compare_inode_map_callback(const void *a, const void *b)
|
|
{
|
|
return *((ext2_ino_t*)a) - *((ext2_ino_t*)b);
|
|
}
|
|
|
|
void realloc_sort_inode_map(realloc_data *rd)
|
|
{
|
|
if (!rd->inode_map)
|
|
{
|
|
return;
|
|
}
|
|
qsort(rd->inode_map, rd->inode_map_size, sizeof(ext2_ino_t)*2, realloc_compare_inode_map_callback);
|
|
}
|
|
|
|
ext2_ino_t realloc_search_inode_map(realloc_data *rd, ext2_ino_t old)
|
|
{
|
|
__u32 start = 0, end = rd->inode_map_size, cur;
|
|
ext2_ino_t cur_ino;
|
|
if (!rd->inode_map)
|
|
{
|
|
return 0;
|
|
}
|
|
while (end-start > 1)
|
|
{
|
|
cur = (start+end)>>1;
|
|
cur_ino = rd->inode_map[cur<<1];
|
|
if (cur_ino < old)
|
|
{
|
|
start = cur+1;
|
|
}
|
|
else if (cur_ino > old)
|
|
{
|
|
end = cur;
|
|
}
|
|
else
|
|
{
|
|
return rd->inode_map[(cur<<1)+1];
|
|
}
|
|
}
|
|
if (rd->inode_map[start<<1] == old)
|
|
{
|
|
return rd->inode_map[(start<<1)+1];
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Rewrite extents */
|
|
static errcode_t rewrite_extents(ext2_filsys fs, ext2_ino_t ino)
|
|
{
|
|
ext2_extent_handle_t handle;
|
|
struct ext2fs_extent extent;
|
|
errcode_t errcode;
|
|
struct ext2_extent_info info;
|
|
|
|
errcode = ext2fs_extent_open(fs, ino, &handle);
|
|
if (errcode)
|
|
return errcode;
|
|
|
|
errcode = ext2fs_extent_get(handle, EXT2_EXTENT_ROOT, &extent);
|
|
if (errcode)
|
|
goto out;
|
|
|
|
do {
|
|
errcode = ext2fs_extent_get_info(handle, &info);
|
|
if (errcode)
|
|
break;
|
|
|
|
/*
|
|
* If this is the first extent in an extent block that we
|
|
* haven't visited, rewrite the extent to force the ETB
|
|
* checksum to be rewritten.
|
|
*/
|
|
if (info.curr_entry == 1 && info.curr_level != 0 &&
|
|
!(extent.e_flags & EXT2_EXTENT_FLAGS_SECOND_VISIT)) {
|
|
errcode = ext2fs_extent_replace(handle, 0, &extent);
|
|
if (errcode)
|
|
break;
|
|
}
|
|
|
|
/* Skip to the end of a block of leaf nodes */
|
|
if (extent.e_flags & EXT2_EXTENT_FLAGS_LEAF) {
|
|
errcode = ext2fs_extent_get(handle,
|
|
EXT2_EXTENT_LAST_SIB,
|
|
&extent);
|
|
if (errcode)
|
|
break;
|
|
}
|
|
|
|
errcode = ext2fs_extent_get(handle, EXT2_EXTENT_NEXT, &extent);
|
|
} while (errcode == 0);
|
|
|
|
out:
|
|
/* Ok if we run off the end */
|
|
if (errcode == EXT2_ET_EXTENT_NO_NEXT)
|
|
errcode = 0;
|
|
ext2fs_extent_free(handle);
|
|
return errcode;
|
|
}
|
|
|
|
/**
|
|
* Move inodes from the end of each block group inode table
|
|
* so the tables can be shrinked
|
|
*/
|
|
errcode_t shrink_move_inodes(realloc_data *rd)
|
|
{
|
|
errcode_t retval = 0;
|
|
int inode_size = EXT2_INODE_SIZE(rd->fs->super);
|
|
__u32 group, i;
|
|
__u32 new_group;
|
|
ext2_ino_t ino, new_ino;
|
|
struct ext2_inode *inode = NULL;
|
|
if (retval)
|
|
{
|
|
return retval;
|
|
}
|
|
retval = ext2fs_get_mem(inode_size, &inode);
|
|
if (retval)
|
|
{
|
|
return retval;
|
|
}
|
|
for (group = 0; group < rd->fs->group_desc_count; group++)
|
|
{
|
|
for (i = rd->ig_new; i < rd->ig_old; i++)
|
|
{
|
|
ino = 1 + group*rd->ig_old + i;
|
|
if (ext2fs_test_inode_bitmap2(rd->fs->inode_map, ino))
|
|
{
|
|
// Inode is occupied and should be moved
|
|
new_group = group;
|
|
do
|
|
{
|
|
retval = ext2fs_find_first_zero_inode_bitmap2(rd->fs->inode_map,
|
|
1 + new_group*rd->ig_old,
|
|
new_group*rd->ig_old+rd->ig_new, &new_ino);
|
|
if (!retval)
|
|
{
|
|
break;
|
|
}
|
|
new_group = (new_group+1) % rd->fs->group_desc_count;
|
|
} while (new_group != group);
|
|
if (retval)
|
|
{
|
|
// No space to move this inode
|
|
goto out;
|
|
}
|
|
// Copy inode to the new place
|
|
check_inode_uninit(rd->fs, rd->fs->inode_map, new_group);
|
|
retval = ext2fs_read_inode_full(rd->fs, ino, inode, inode_size);
|
|
if (retval)
|
|
{
|
|
goto out;
|
|
}
|
|
retval = ext2fs_write_inode_full(rd->fs, new_ino, inode, inode_size);
|
|
if (retval)
|
|
{
|
|
goto out;
|
|
}
|
|
ext2fs_inode_alloc_stats2(rd->fs, new_ino, 1, inode->i_mode & S_IFDIR);
|
|
ext2fs_inode_alloc_stats2(rd->fs, ino, -1, inode->i_mode & S_IFDIR);
|
|
/* Rewrite extent block checksums with new inode number */
|
|
if (ext2fs_has_feature_metadata_csum(rd->fs->super) &&
|
|
(inode->i_flags & EXT4_EXTENTS_FL)) {
|
|
rd->fs->flags |= EXT2_FLAG_IGNORE_CSUM_ERRORS;
|
|
retval = rewrite_extents(rd->fs, new_ino);
|
|
rd->fs->flags &= ~EXT2_FLAG_IGNORE_CSUM_ERRORS;
|
|
if (retval)
|
|
goto out;
|
|
}
|
|
// Remember mapping
|
|
realloc_add_inode_map(rd, ino, new_ino);
|
|
}
|
|
}
|
|
}
|
|
if (rd->inode_map_size)
|
|
{
|
|
ext2fs_mark_ib_dirty(rd->fs);
|
|
}
|
|
out:
|
|
if (inode)
|
|
{
|
|
ext2fs_free_mem(&inode);
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
/**
|
|
* Move data blocks from the new place for each block group inode table
|
|
* so the tables can be grown or moved.
|
|
*/
|
|
errcode_t extend_move_blocks(realloc_data *rd)
|
|
{
|
|
ext2fs_block_bitmap reserve_map;
|
|
dgrp_t grp;
|
|
errcode_t retval;
|
|
if (rd->ibg_new == rd->ibg_old)
|
|
{
|
|
return 0;
|
|
}
|
|
retval = ext2fs_allocate_block_bitmap(rd->fs, "reserved block map", &reserve_map);
|
|
if (retval)
|
|
{
|
|
return retval;
|
|
}
|
|
// Mark blocks we want to free as "reserved"
|
|
// Don't care about which blocks are already used by inode tables,
|
|
// because ext2fs_move_blocks only moves blocks that belong to inodes.
|
|
for (grp = 0; grp < rd->fs->group_desc_count; grp++)
|
|
{
|
|
ext2fs_mark_block_bitmap_range2(reserve_map, rd->new_itable_loc[grp], rd->ibg_new);
|
|
}
|
|
retval = ext2fs_move_blocks(rd->fs, reserve_map, rd->fs->block_map, 0);
|
|
ext2fs_mark_bb_dirty(rd->fs);
|
|
ext2fs_flush(rd->fs);
|
|
ext2fs_free_block_bitmap(reserve_map);
|
|
return retval;
|
|
}
|
|
|
|
static int change_inode_numbers_callback(ext2_ino_t dir, int entry,
|
|
struct ext2_dir_entry *dirent, int offset,
|
|
int blocksize, char *buf, void *priv_data)
|
|
{
|
|
realloc_data *rd = priv_data;
|
|
ext2_ino_t new_ino = realloc_search_inode_map(rd, dirent->inode);
|
|
if (!new_ino)
|
|
{
|
|
new_ino = dirent->inode;
|
|
}
|
|
new_ino = 1 + (new_ino-1)/rd->ig_old*rd->ig_new + (new_ino-1)%rd->ig_old;
|
|
if (new_ino != dirent->inode)
|
|
{
|
|
dirent->inode = new_ino;
|
|
return DIRENT_CHANGED;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int fix_dirent_csum_callback(ext2_ino_t dir, int entry,
|
|
struct ext2_dir_entry *dirent, int offset,
|
|
int blocksize, char *buf, void *priv_data)
|
|
{
|
|
realloc_data *rd = priv_data;
|
|
ext2fs_dir_block_csum_set(rd->fs, dir, dirent);
|
|
return DIRENT_CHANGED;
|
|
}
|
|
|
|
/**
|
|
* Change inode numbers in all directory entries
|
|
*/
|
|
errcode_t change_inode_numbers(realloc_data *rd)
|
|
{
|
|
ext2_ino_t ino;
|
|
realloc_sort_inode_map(rd);
|
|
for (ino = 1; ino <= rd->fs->super->s_inodes_count; ino++)
|
|
{
|
|
if (ext2fs_test_inode_bitmap2(rd->fs->inode_map, ino))
|
|
{
|
|
ext2fs_dir_iterate2(rd->fs, ino, 0, 0, change_inode_numbers_callback, rd);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
errcode_t fix_dirent_csums(realloc_data *rd)
|
|
{
|
|
ext2_ino_t ino;
|
|
realloc_sort_inode_map(rd);
|
|
for (ino = 1; ino <= rd->fs->super->s_inodes_count; ino++)
|
|
{
|
|
if (ext2fs_test_inode_bitmap2(rd->fs->inode_map, ino))
|
|
{
|
|
ext2fs_dir_iterate2(rd->fs, ino, 0, 0, fix_dirent_csum_callback, rd);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* 1) Move inode tables
|
|
* 2) Mark/unmark new/old inode table blocks
|
|
* 3) Adjust superblock and block group descriptors
|
|
*/
|
|
errcode_t change_super_and_bgd(realloc_data *rd)
|
|
{
|
|
blk64_t blk, end;
|
|
dgrp_t grp, n_grp, flex_grp;
|
|
__u32 used_ig, used_ibg, i, unus;
|
|
ext2_ino_t inum;
|
|
errcode_t retval = 0;
|
|
int has_gdt_csum = EXT2_HAS_RO_COMPAT_FEATURE(rd->fs->super, EXT4_FEATURE_RO_COMPAT_GDT_CSUM|EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
|
|
int cl = EXT2FS_CLUSTER_RATIO(rd->fs);
|
|
void *buf = NULL, *inode_buf = NULL;
|
|
ext2fs_flush(rd->fs);
|
|
retval = ext2fs_get_mem(EXT2_BLOCK_SIZE(rd->fs->super) * rd->ibg_new * rd->flexbg_size, &buf);
|
|
if (retval)
|
|
{
|
|
goto out;
|
|
}
|
|
// First mark/unmark inode table blocks (mark and unmark in separate passes)
|
|
for (grp = 0; grp < rd->fs->group_desc_count; grp++)
|
|
{
|
|
// In case of bigalloc, last cluster of previous group inode table
|
|
// may often overlap with the first cluster of next group one
|
|
blk = ext2fs_inode_table_loc(rd->fs, grp) & ~(cl-1);
|
|
if (!ext2fs_test_block_bitmap2(rd->fs->block_map, blk))
|
|
{
|
|
blk += cl;
|
|
}
|
|
end = (ext2fs_inode_table_loc(rd->fs, grp) + rd->ibg_old + (cl-1)) & ~(cl-1);
|
|
for (; blk < end; blk += cl)
|
|
{
|
|
ext2fs_block_alloc_stats2(rd->fs, blk, -1);
|
|
}
|
|
}
|
|
for (grp = 0; grp < rd->fs->group_desc_count; grp++)
|
|
{
|
|
blk = rd->new_itable_loc[grp] & ~(cl-1);
|
|
if (grp > 0 && blk == end-cl)
|
|
{
|
|
blk += cl;
|
|
}
|
|
end = (rd->new_itable_loc[grp] + rd->ibg_new + (cl-1)) & ~(cl-1);
|
|
for (; blk < end; blk += cl)
|
|
{
|
|
if (ext2fs_test_block_bitmap2(rd->fs->block_map, blk))
|
|
{
|
|
printf("BUG: Block %lld (suited for group %u inode table) is still allocated\n", blk, grp);
|
|
retval = ENOSPC;
|
|
goto out;
|
|
}
|
|
ext2fs_block_alloc_stats2(rd->fs, blk, +1);
|
|
}
|
|
}
|
|
for (flex_grp = 0; flex_grp < rd->flex_count; flex_grp++)
|
|
{
|
|
// Read inode tables for a flex_bg
|
|
grp = flex_grp*rd->flexbg_size;
|
|
n_grp = min(rd->flexbg_size, rd->fs->group_desc_count-grp);
|
|
for (i = 0; i < n_grp; i++, grp++)
|
|
{
|
|
// Skip unitialized inode table parts
|
|
used_ibg = rd->ibg_old;
|
|
used_ig = rd->ig_old;
|
|
if (has_gdt_csum)
|
|
{
|
|
if (ext2fs_bg_flags_test(rd->fs, grp, EXT2_BG_INODE_UNINIT))
|
|
{
|
|
used_ig = used_ibg = 0;
|
|
}
|
|
else
|
|
{
|
|
used_ig = (rd->ig_old - ext2fs_bg_itable_unused(rd->fs, grp));
|
|
used_ibg = (used_ig * EXT2_INODE_SIZE(rd->fs->super)+EXT2_BLOCK_SIZE(rd->fs->super)-1)/EXT2_BLOCK_SIZE(rd->fs->super);
|
|
}
|
|
}
|
|
blk = ext2fs_inode_table_loc(rd->fs, grp);
|
|
if (used_ibg > 0)
|
|
{
|
|
inode_buf = buf + i*rd->ibg_new*EXT2_BLOCK_SIZE(rd->fs->super);
|
|
retval = io_channel_read_blk64(rd->fs->io, blk, min(used_ibg, rd->ibg_new), inode_buf);
|
|
if (retval)
|
|
goto out;
|
|
if (used_ig < rd->ig_new)
|
|
{
|
|
memset(inode_buf + EXT2_INODE_SIZE(rd->fs->super) * used_ig,
|
|
0, EXT2_INODE_SIZE(rd->fs->super) * (rd->ig_new - used_ig));
|
|
}
|
|
if (ext2fs_has_feature_metadata_csum(rd->fs->super))
|
|
{
|
|
for (inum = 0; inum < used_ig || inum < rd->ig_new; inum++)
|
|
{
|
|
retval = ext2fs_inode_csum_set(
|
|
rd->fs, 1 + rd->ig_new*grp + inum,
|
|
inode_buf + EXT2_INODE_SIZE(rd->fs->super) * inum
|
|
);
|
|
if (retval)
|
|
goto out;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Write inode tables
|
|
grp = flex_grp*rd->flexbg_size;
|
|
for (i = 0; i < n_grp; i++, grp++)
|
|
{
|
|
retval = io_channel_write_blk64(rd->fs->io, rd->new_itable_loc[grp], rd->ibg_new,
|
|
buf + i*rd->ibg_new*EXT2_BLOCK_SIZE(rd->fs->super));
|
|
if (retval)
|
|
{
|
|
printf("Error moving inode table for block group %u\n", grp);
|
|
goto out;
|
|
}
|
|
// Set inode table location and free inode count
|
|
ext2fs_inode_table_loc_set(rd->fs, grp, rd->new_itable_loc[grp]);
|
|
ext2fs_bg_free_inodes_count_set(rd->fs, grp,
|
|
ext2fs_bg_free_inodes_count(rd->fs, grp) + rd->ig_new - rd->ig_old);
|
|
if (has_gdt_csum)
|
|
{
|
|
unus = ext2fs_bg_itable_unused(rd->fs, grp);
|
|
if (rd->ig_new > rd->ig_old || unus >= rd->ig_old - rd->ig_new)
|
|
{
|
|
unus += rd->ig_new - rd->ig_old;
|
|
}
|
|
else
|
|
{
|
|
unus = 0;
|
|
}
|
|
ext2fs_bg_itable_unused_set(rd->fs, grp, unus);
|
|
ext2fs_group_desc_csum_set(rd->fs, grp);
|
|
}
|
|
}
|
|
}
|
|
// Bitmaps never need to be moved because a single bitmap is always a single FS block
|
|
ext2fs_mark_bb_dirty(rd->fs);
|
|
retval = rd->fs->write_bitmaps(rd->fs);
|
|
if (retval)
|
|
{
|
|
goto out;
|
|
}
|
|
rd->fs->write_bitmaps = NULL;
|
|
// Explicitly set 'overwrite backup superblocks' flag
|
|
rd->fs->flags &= ~EXT2_FLAG_MASTER_SB_ONLY;
|
|
rd->fs->super->s_free_inodes_count += rd->fs->group_desc_count * (rd->ig_new - rd->ig_old);
|
|
rd->fs->super->s_inodes_per_group = rd->ig_new;
|
|
rd->fs->super->s_inodes_count = rd->fs->group_desc_count * rd->ig_new;
|
|
ext2fs_mark_super_dirty(rd->fs);
|
|
if (rd->ig_new > rd->ig_old)
|
|
{
|
|
// Mark newly allocated inodes as free in the bitmap
|
|
__u32 ino;
|
|
ext2fs_read_inode_bitmap(rd->fs);
|
|
for (grp = 0; grp < rd->fs->group_desc_count; grp++)
|
|
{
|
|
for (ino = rd->ig_old; ino < rd->ig_new; ino++)
|
|
{
|
|
ext2fs_unmark_inode_bitmap2(rd->fs->inode_map, 1 + ino + grp*rd->ig_new);
|
|
}
|
|
}
|
|
ext2fs_mark_ib_dirty(rd->fs);
|
|
}
|
|
out:
|
|
if (buf)
|
|
{
|
|
ext2fs_free_mem(&buf);
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
int nonmovable_callback(ext2_filsys fs, blk64_t *blocknr, e2_blkcnt_t blockcnt, blk64_t ref_blk, int ref_offset, void *priv_data)
|
|
{
|
|
if (blockcnt >= 0)
|
|
{
|
|
ext2fs_mark_block_bitmap2((ext2fs_block_bitmap)priv_data, *blocknr);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Allocate new place for all groups' inode tables and remember it.
|
|
* This is more correct, because allows us to correctly handle situations
|
|
* when flex_bg is so big that inode tables for all groups in flex_bg
|
|
* do not fit into its first group, and also allows us to honor bad blocks.
|
|
*/
|
|
errcode_t alloc_itables(realloc_data *rd)
|
|
{
|
|
errcode_t retval = 0;
|
|
ext2fs_block_bitmap nonmovable = NULL;
|
|
dgrp_t grp, flex_grp;
|
|
int n_grp, i, mod;
|
|
blk64_t blk, start, end;
|
|
retval = ext2fs_get_mem(sizeof(blk64_t) * rd->fs->group_desc_count, &rd->new_itable_loc);
|
|
if (retval)
|
|
goto out;
|
|
// Create a map of blocks we can't move
|
|
retval = ext2fs_allocate_block_bitmap(rd->fs, "non-movable block bitmap", &nonmovable);
|
|
if (retval < 0)
|
|
goto out;
|
|
retval = ext2fs_block_iterate3(rd->fs, EXT2_BAD_INO, 0, NULL, nonmovable_callback, nonmovable);
|
|
if (retval < 0)
|
|
goto out;
|
|
retval = ext2fs_block_iterate3(rd->fs, EXT2_RESIZE_INO, 0, NULL, nonmovable_callback, nonmovable);
|
|
if (retval < 0)
|
|
goto out;
|
|
for (grp = 0; grp < rd->fs->group_desc_count; grp++)
|
|
{
|
|
ext2fs_reserve_super_and_bgd(rd->fs, grp, nonmovable);
|
|
ext2fs_mark_block_bitmap2(nonmovable, ext2fs_block_bitmap_loc(rd->fs, grp));
|
|
ext2fs_mark_block_bitmap2(nonmovable, ext2fs_inode_bitmap_loc(rd->fs, grp));
|
|
}
|
|
// flex_bg parameters
|
|
if (EXT2_HAS_INCOMPAT_FEATURE(rd->fs->super, EXT4_FEATURE_INCOMPAT_FLEX_BG)
|
|
&& rd->fs->super->s_log_groups_per_flex)
|
|
{
|
|
rd->flexbg_size = 1 << rd->fs->super->s_log_groups_per_flex;
|
|
}
|
|
else
|
|
{
|
|
rd->flexbg_size = 1;
|
|
}
|
|
rd->flex_count = (rd->fs->group_desc_count + rd->flexbg_size - 1) / rd->flexbg_size;
|
|
// Allocate inode tables
|
|
for (flex_grp = 0; flex_grp < rd->flex_count; flex_grp++)
|
|
{
|
|
grp = flex_grp*rd->flexbg_size;
|
|
n_grp = min(rd->flexbg_size, rd->fs->group_desc_count-grp);
|
|
// TODO We could use a better algorithm that would always try to find
|
|
// the biggest free sequence of blocks if it can't allocate all inode
|
|
// tables in sequence
|
|
start = ext2fs_group_first_block2(rd->fs, grp);
|
|
end = ext2fs_group_last_block2(rd->fs, grp+n_grp-1);
|
|
mod = 0;
|
|
for (i = 0; i < n_grp; i++, grp++)
|
|
{
|
|
retval = ext2fs_get_free_blocks2(rd->fs, start, end, rd->ibg_new, nonmovable, &blk);
|
|
if (retval)
|
|
goto out;
|
|
if (start == blk)
|
|
blk += mod;
|
|
rd->new_itable_loc[grp] = blk;
|
|
start = blk + rd->ibg_new;
|
|
// Allow adjacent inode tables to share one cluster
|
|
// ext2fs_get_free_blocks2 operates on cluster boundaries,
|
|
// so we handle the division residue separately
|
|
mod = start & EXT2FS_CLUSTER_MASK(rd->fs);
|
|
start = start - mod;
|
|
}
|
|
}
|
|
out:
|
|
if (nonmovable)
|
|
ext2fs_free_block_bitmap(nonmovable);
|
|
return retval;
|
|
}
|
|
|
|
/**
|
|
* Main function: change inode number of a filesystem!
|
|
*/
|
|
errcode_t do_realloc(realloc_data *rd)
|
|
{
|
|
__u32 ig_round;
|
|
errcode_t retval;
|
|
int phase = 1;
|
|
rd->ig_old = EXT2_INODES_PER_GROUP(rd->fs->super);
|
|
rd->ig_new = rd->new_inode_count / rd->fs->group_desc_count;
|
|
// inodes-per-group must be a multiple of 8 so each byte of inode bitmap is filled
|
|
rd->ig_new &= ~7;
|
|
if (rd->ig_new < 16)
|
|
{
|
|
printf("Too small number of inodes requested (%u), min inodes per group = 16\n", rd->ig_new);
|
|
retval = EINVAL;
|
|
goto out;
|
|
}
|
|
rd->ibg_old = rd->fs->inode_blocks_per_group;
|
|
rd->ibg_new = (rd->ig_new * EXT2_INODE_SIZE(rd->fs->super) +
|
|
EXT2_BLOCK_SIZE(rd->fs->super) - 1) / EXT2_BLOCK_SIZE(rd->fs->super);
|
|
if (rd->new_inode_count != rd->ig_new * rd->fs->group_desc_count)
|
|
{
|
|
printf("Inode count %u rounded down to %u = (%u inodes per group) * (%u block groups)\n",
|
|
rd->new_inode_count, rd->ig_new * rd->fs->group_desc_count, rd->ig_new, rd->fs->group_desc_count);
|
|
}
|
|
rd->new_inode_count = rd->ig_new * rd->fs->group_desc_count;
|
|
ig_round = rd->ibg_new * EXT2_BLOCK_SIZE(rd->fs->super) / EXT2_INODE_SIZE(rd->fs->super);
|
|
if (rd->ig_new != ig_round)
|
|
{
|
|
printf("Inode count %u is not optimal because %u inodes per group is not a multiple of %u"
|
|
" - there will be wasted space in inode tables. Optimal inode count would be %u.\n",
|
|
rd->new_inode_count, rd->ig_new, EXT2_BLOCK_SIZE(rd->fs->super) / EXT2_INODE_SIZE(rd->fs->super), ig_round);
|
|
}
|
|
ext2fs_read_bitmaps(rd->fs);
|
|
// Find where to put the new inode tables
|
|
retval = alloc_itables(rd);
|
|
if (retval)
|
|
{
|
|
goto out;
|
|
}
|
|
if (rd->ig_new < rd->ig_old)
|
|
{
|
|
if (rd->new_inode_count < rd->fs->super->s_inodes_count - rd->fs->super->s_free_inodes_count)
|
|
{
|
|
printf("Too small number of inodes requested, existing inodes (%u) won't fit\n",
|
|
rd->fs->super->s_inodes_count - rd->fs->super->s_free_inodes_count);
|
|
retval = ENOSPC;
|
|
goto out;
|
|
}
|
|
printf("Phase %d: Moving inodes out of the way\n", phase++);
|
|
retval = shrink_move_inodes(rd);
|
|
if (retval)
|
|
{
|
|
goto out;
|
|
}
|
|
}
|
|
else if (rd->ig_new > rd->ig_old)
|
|
{
|
|
blk64_t required_blocks = (rd->ibg_new - rd->ibg_old) * rd->fs->group_desc_count;
|
|
if (required_blocks > ext2fs_free_blocks_count(rd->fs->super))
|
|
{
|
|
printf("Requested number of inodes is too big, it requires at least %llu free blocks, "
|
|
"and there are only %llu free blocks available\n",
|
|
required_blocks, ext2fs_free_blocks_count(rd->fs->super));
|
|
retval = ENOSPC;
|
|
goto out;
|
|
}
|
|
}
|
|
else if (rd->ibg_new == rd->ibg_old)
|
|
{
|
|
printf("The requested number of inodes is equal to current\n");
|
|
goto out;
|
|
}
|
|
printf("Phase %d: Moving data blocks out of the way\n", phase++);
|
|
retval = extend_move_blocks(rd);
|
|
if (retval)
|
|
{
|
|
goto out;
|
|
}
|
|
printf("Phase %d: Changing all inode numbers\n", phase++);
|
|
retval = change_inode_numbers(rd);
|
|
if (retval)
|
|
{
|
|
goto out;
|
|
}
|
|
printf("Phase %d: Adjusting superblock and block group descriptors\n", phase++);
|
|
retval = change_super_and_bgd(rd);
|
|
if (retval)
|
|
{
|
|
goto out;
|
|
}
|
|
/* printf("Phase %d: Fixing all directory checksums\n", phase++);
|
|
retval = fix_dirent_csums(rd);
|
|
if (retval)
|
|
{
|
|
goto out;
|
|
}*/
|
|
out:
|
|
if (rd->new_itable_loc)
|
|
{
|
|
ext2fs_free_mem(&rd->new_itable_loc);
|
|
}
|
|
if (rd->inode_map)
|
|
{
|
|
free(rd->inode_map);
|
|
rd->inode_map = NULL;
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
__u32 atou(char *s)
|
|
{
|
|
__u32 x = 0;
|
|
if (s[0] == '0')
|
|
{
|
|
if (s[1] == 'x' || s[1] == 'X')
|
|
{
|
|
sscanf(s+2, "%x", &x);
|
|
}
|
|
else
|
|
{
|
|
sscanf(s+1, "%o", &x);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sscanf(s, "%u", &x);
|
|
}
|
|
return x;
|
|
}
|
|
|
|
const char *program_name = "realloc-inodes";
|
|
|
|
static int setup_patch_io(char *name, char *patch_file, io_manager *io_ptr)
|
|
{
|
|
set_patch_io_backing_manager(*io_ptr);
|
|
set_patch_io_patch_file(patch_file);
|
|
*io_ptr = patch_io_manager;
|
|
printf(
|
|
"To make backup before applying changes run:\n"
|
|
" e2patch backup %s %s %s.backup\n"
|
|
"Then, to apply the inode change operation to the real filesystem run:\n"
|
|
" e2patch apply %s %s\n"
|
|
, name, patch_file, patch_file, name, patch_file);
|
|
return 0;
|
|
}
|
|
|
|
int main(int narg, char **args)
|
|
{
|
|
realloc_data rd = { 0 };
|
|
int optind, retval, io_flags = 0, force = 0;
|
|
io_manager io_ptr = unix_io_manager;
|
|
struct stat64 st_buf;
|
|
for (optind = 1; optind < narg; optind++)
|
|
{
|
|
if (!strcmp(args[optind], "--patch"))
|
|
rd.patch_file = args[++optind];
|
|
else if (!strcmp(args[optind], "--force"))
|
|
force = 1;
|
|
else if (args[optind][0] == '-' && args[optind][1] == '-')
|
|
{
|
|
if (strcmp(args[optind], "--help") != 0)
|
|
printf("Unknown option: %s\n", args[optind]);
|
|
break;
|
|
}
|
|
else if (!rd.device_name)
|
|
rd.device_name = args[optind];
|
|
else
|
|
rd.new_inode_count = atou(args[optind++]);
|
|
}
|
|
if (!rd.device_name || !rd.new_inode_count)
|
|
{
|
|
printf(
|
|
"Change inode count of an ext2/ext3/ext4 filesystem\n"
|
|
"License: GNU GPLv2 or later\nCopyright (c) Vitaliy Filippov, 2013+\n\n"
|
|
"USAGE: ./realloc-inodes [--patch <patch_file>] <device> <new_inode_count>\n\n"
|
|
"If <patch_file> is specified, all modifications are written to it\n"
|
|
"instead of directly modifying the filesystem. These modifications\n"
|
|
"can then be applied and unapplied to the real filesystem in a safe way.\n"
|
|
"<patch_file> should be on a filesystem supporting sparse files.\n"
|
|
);
|
|
return 0;
|
|
}
|
|
add_error_table(&et_ext2_error_table);
|
|
// Open FS
|
|
rd.fs_fd = ext2fs_open_file(rd.device_name, O_RDWR, 0);
|
|
if (rd.fs_fd < 0)
|
|
{
|
|
com_err(program_name, errno, _("while opening %s"), rd.device_name);
|
|
exit(1);
|
|
}
|
|
// FIXME Don't know why, but it segfaults if I use ext2fs_fstat here! (of course with ext2fs_struct_stat)
|
|
retval = fstat64(rd.fs_fd, &st_buf);
|
|
if (retval < 0)
|
|
{
|
|
com_err(program_name, errno, _("while getting stat information for %s"), rd.device_name);
|
|
exit(1);
|
|
}
|
|
if (!S_ISREG(st_buf.st_mode))
|
|
{
|
|
close(rd.fs_fd);
|
|
rd.fs_fd = -1;
|
|
}
|
|
rd.io_options = strchr(rd.device_name, '?');
|
|
if (rd.io_options)
|
|
{
|
|
*rd.io_options++ = 0;
|
|
}
|
|
if (rd.patch_file)
|
|
{
|
|
setup_patch_io(rd.device_name, rd.patch_file, &io_ptr);
|
|
}
|
|
io_flags = EXT2_FLAG_64BITS | EXT2_FLAG_RW | EXT2_FLAG_EXCLUSIVE;
|
|
retval = ext2fs_open2(rd.device_name, rd.io_options, io_flags, 0, 0, io_ptr, &rd.fs);
|
|
if (retval)
|
|
{
|
|
com_err(program_name, retval, _("while trying to open %s"), rd.device_name);
|
|
printf(_("Couldn't find valid filesystem superblock.\n"));
|
|
goto close_fd;
|
|
}
|
|
if (!force)
|
|
{
|
|
if ((rd.fs->super->s_state & EXT2_ERROR_FS) ||
|
|
!(rd.fs->super->s_state & EXT2_VALID_FS) ||
|
|
(rd.fs->super->s_feature_incompat & EXT3_FEATURE_INCOMPAT_RECOVER))
|
|
{
|
|
fprintf(stderr, _("Please run 'e2fsck -f %s' first.\n\n"), rd.device_name);
|
|
goto close_fs;
|
|
}
|
|
if (rd.fs->super->s_feature_incompat & ~REALLOC_FEATURE_INCOMPAT_SUPP ||
|
|
rd.fs->super->s_feature_compat & ~REALLOC_FEATURE_COMPAT_SUPP ||
|
|
rd.fs->super->s_feature_ro_compat & ~REALLOC_FEATURE_RO_COMPAT_SUPP)
|
|
{
|
|
retval = EXT2_ET_UNSUPP_FEATURE;
|
|
com_err(program_name, retval, _("while trying to open %s"), rd.device_name);
|
|
goto close_fs;
|
|
}
|
|
}
|
|
// Call main realloc function
|
|
retval = do_realloc(&rd);
|
|
if (retval)
|
|
{
|
|
com_err(program_name, retval, _("while resizing inode count"));
|
|
goto close_fs;
|
|
}
|
|
close_fs:
|
|
ext2fs_close(rd.fs);
|
|
close_fd:
|
|
if (rd.fs_fd > 0)
|
|
{
|
|
close(rd.fs_fd);
|
|
}
|
|
return retval;
|
|
}
|