サイズの大きいファイルをコピーした後で、ハッシュ値を比較したら、コピー元とコピー先が一致しなかったとする。ファイル全体をコピーし直すと、時間がかかる。壊れている範囲が一部だけなら、cmpで差分のあるバイト範囲を特定して、ddでその範囲だけを書き直せる。
$ cmp src.bin dst.bin
src.bin dst.bin differ: char 5242891, line 20492
$ dd if=src.bin of=dst.bin bs=1048576 skip=5 seek=5 count=1 conv=notrunc
1つ目のコマンドで、最初に異なるバイトの位置を確認する。2つ目のコマンドで、1MiB単位のブロックを指定して、コピー元からコピー先へ書き込む。bs=1048576は1MiBである。実行例は、20MiBのファイルの2か所を壊したもの。macOS 27.0.1で実行した。
参考: 【shasum】コピー前後のファイルをSHA-256で比較して検証する (-a 256)
cmp -lで差分のある位置をすべて表示する
cmpは、オプションなしでは、最初に異なる位置だけを表示して終了する。-lを付けると、異なるバイトをすべて表示する。
$ cmp -l src.bin dst.bin | head -4
5242891 152 130
5242892 121 131
5242893 273 132
13631489 305 0
各行は、バイトの位置(1から数える10進数)と、コピー元とコピー先のバイトの値(8進数)である。壊れた範囲が広いと、行数も多くなる。2MiBを書き換えたファイルでは、約200万行になった。
$ cmp -l src.bin dst.bin | wc -l
2088974
ファイルに保存せず、awkにパイプして、範囲に集約する。
差分のあるブロックの範囲を求める
バイトの位置を1MiB(1048576バイト)で割ると、ブロックの番号が得られる。awkで、連続するブロックを1つの範囲にまとめる。
$ cmp -l src.bin dst.bin | awk -v bs=1048576 '
{ b = int(($1 - 1) / bs)
if (NR == 1) { s = b; e = b; next }
if (b <= e + 1) { e = b } else { print s, e; s = b; e = b } }
END { if (NR > 0) print s, e }'
5 5
13 14
ブロック5と、ブロック13から14に差分がある。位置5242891は、5242890 ÷ 1048576の商が5なので、ブロック5に含まれる。
ddで範囲だけを書き直す
ddのskipは、入力の読み込みを始めるブロック番号で、seekは、出力の書き込みを始めるブロック番号である。同じ番号を指定して、同じ位置に書き込む。countは、ブロックの数である。
$ dd if=src.bin of=dst.bin bs=1048576 skip=5 seek=5 count=1 conv=notrunc
$ dd if=src.bin of=dst.bin bs=1048576 skip=13 seek=13 count=2 conv=notrunc
書き直した後で、cmpで比較する。差分がなければ、何も出力されず、終了コードは0である。
$ cmp src.bin dst.bin && echo SAME
SAME
conv=notruncを付ける
conv=notruncは、書き込み前に出力ファイルを切り詰めない指定である。付けないと、ddは出力ファイルを書き込んだ位置で切り詰める。
$ cp src.bin t.bin
$ dd if=src.bin of=t.bin bs=1048576 skip=5 seek=5 count=1 2>/dev/null
$ wc -c < t.bin
6291456
$ cp src.bin t.bin
$ dd if=src.bin of=t.bin bs=1048576 skip=5 seek=5 count=1 conv=notrunc 2>/dev/null
$ wc -c < t.bin
20971520
conv=notruncなしでは、ファイルのサイズが6291456バイト(6MiB)に縮んだ。conv=notruncありでは、元のサイズの20971520バイトが保たれた。コピー先のファイルを書き直すときは、必ず付ける。
ブロックサイズは数値で指定する
ブロックサイズにbs=1mと書くと、macOSの/bin/ddでは動作するが、GNU coreutilsのddではinvalid numberで失敗する。
$ dd if=src.bin of=/dev/null bs=1m count=1
dd: invalid number: '1m'
$ dd if=src.bin of=/dev/null bs=1M count=1
1+0 records in
1+0 records out
bs=1Mは、どちらでも動作する。macOSのHomebrewでcoreutilsをインストールして、gnubinをPATHに追加している場合は、ddがGNU版になる。type -a ddで、どのddが実行されるか確認できる。
$ type -a dd
dd is /opt/homebrew/opt/coreutils/libexec/gnubin/dd
dd is /bin/dd
bs=1048576のように数値で書けば、どちらのddでも動作する。
スクリプトにまとめる
範囲の特定から書き直しまでを、1つのスクリプトにまとめる。
#!/bin/bash
# usage: rewrite-diff-ranges.sh <src> <dst>
# rewrites only the 1 MiB blocks of <dst> that differ from <src> (same size files only)
src=$1
dst=$2
bs=1048576
cmp -l "$src" "$dst" |
awk -v bs="$bs" '
{ b = int(($1 - 1) / bs)
if (NR == 1) { s = b; e = b; next }
if (b <= e + 1) { e = b } else { print s, e; s = b; e = b } }
END { if (NR > 0) print s, e }' |
while read -r s e; do
echo "rewrite blocks $s-$e"
dd if="$src" of="$dst" bs="$bs" skip="$s" seek="$s" count=$((e - s + 1)) conv=notrunc 2>&1 | tail -1
done
cmp "$src" "$dst" && echo SAME
$ ./rewrite-diff-ranges.sh src.bin dst.bin
rewrite blocks 5-5
1048576 bytes (1.0 MB, 1.0 MiB) copied, 0.000385 s, 2.7 GB/s
rewrite blocks 13-14
2097152 bytes (2.1 MB, 2.0 MiB) copied, 0.000497 s, 4.2 GB/s
SAME
差分のないファイルに実行すると、書き込みはされずにSAMEだけが表示される。実行例はmacOSで確認した。Ubuntu 24.04でも、同じ結果になった。
注意点
- コピー元とコピー先のサイズが同じファイルに限る。サイズが異なるファイルは、
cmpがEOF on ...を出力する。cpなどで全体をコピーし直す cmp -lは、差分の有無にかかわらず、2つのファイルを最後まで読み込む。ネットワーク越しのファイルでは、読み込みに時間がかかる- 書き直した後は、ハッシュ値の比較で、ファイル全体が一致したことを確認する
