ルギア君の戯言

雑多な記事。

Binary Difference Generator

MD5とかSHA1とかではどうしても信用できない人の為(?)に2つ("以上"では無い)のバイナリファイルがまったく同じものであるかを調べる為のツールを作った。

[lugia@lugia-castle ~]$ diff --help
使用法: diff [オプション]... FILES   
2つのファイルを行ごとに比較します。  

  -i  --ignore-case  ファイル内容の大文字小文字を無視。
  --ignore-file-name-case  ファイル名の大文字小文字を無視。
  --no-ignore-file-name-case  ファイル名の大文字小文字を区別。
  -E  --ignore-tab-expansion  タブ展開の差を無視。            
  -b  --ignore-space-change  空白数の差を無視。               
  -w  --ignore-all-space  全空白を無視。                      
  -B  --ignore-blank-lines  空白だけの行の差を無視。          
  -i RE  --ignore-matching-lines=RE  REに一致するすべての行の差を無視。
  --strip-trailing-cr  入力から行末キャリッジ・リターンを削除。        
  -a  --text  すべてテキストとして処理。
(後略)

でも用は果たせるかもしれないけど、わかりにくいから作った(ぁ


使い方:

bdiff [オプション] [比較ファイル1] [比較ファイル2]

1にある部分には冒頭に - (マイナス)が、2にある部分には冒頭に + (プラス)が付きます。



オプションは

bdiff -c [数値] -t [数値]

c と t は大文字でも可。
c のあとの数字は一度に読み込む量。32ビットの環境では 4GByte-1 まで指定できる。メモリもそれ相応の量が無いといけないので注意。
t のあとの数字は比較対象となる先頭からのデータ量。16EByte-1 *1まで指定できる。
数値にはスペースなしで単位が付けられる。小文字なら bit、大文字なら Byte。

英字記号 装飾子
k, K キロ
m, M メガ
g, G ギガ

バイト単位で比較するので、ビット単位で指定した場合に生じた端数は切り捨てられる。
また無単位の場合には バイト として扱われる。


0バイトまたはなにも指定されていない場合は、

bdiff -c 16K

と同じ。比較サイズは小さい方のファイルサイズ。


「長い時間が掛かるかもしれません。よろしいですか?」と(gettext の和訳ファイルがなければ英語で)聞かれるので、y と答えると、比較が始まります。
違ったところを発見する度に出力するので、まったく同じなら無出力。


性能はこんなもん。@ Core 2 Duo 1.6GHz、SATA HDD ドライブ (TOSHIBA MK1234GSX)
当然 USB だったり、実際の感覚としては出力があるともっと遅くなる。

[lugia@lugia-castle Documents]$ time ./bdiff McE500-12_Katsuta_Dep.wav McE500-12_Katsuta_Dep.wav
Compare Detail is ...
Chunk Size    :                     16,384 Byte(s)
Total Size    :                 15,067,436 Byte(s)
File 1 (Size) : McE500-12_Katsuta_Dep.wav  (15,067,436)
File 2 (Size) : McE500-12_Katsuta_Dep.wav  (15,067,436)
Verifing may takes very long time. Are you sure? (y/n): y

real    0m4.403s
user    0m0.189s
sys     0m0.018s


同じく PC-PSP 間で。USB ケーブルは製造者不明。PSP は PSP-3000。
USB2.0 の対応/非対応は一切不明。

[lugia@lugia-castle ~]$ time ./bdiff aaa.iso /media/disk/aaa.iso -c 1M
Compare Detail is ...
Chunk Size    :                  1,048,576 Byte(s)
Total Size    :                895,580,160 Byte(s)
File 1 (Size) : aaa.iso                    (895,580,160)
File 2 (Size) : /media/disk/aaa.iso        (895,580,160)
Verifing may takes very long time. Are you sure? (y/n): y

real    1m36.864s
user    0m12.221s
sys     0m1.779s


違う場合には

-0000000000001cb0  61 73 65 3c 2f 62 3e 20 3c 2f 73 70 61 6e 3e 3c
+0000000000001cb0  09 72 65 74 75 72 6e 20 30 34 30 30 3b 0a 20 20

のように出力される。左からファイルの別(-/+)、ファイルの先頭からのバイトオフセット、そこから 16 バイトのバイト列。
なお、データ量が莫大に膨らむ為、パッチには使えない。


ソースコード(続きを読む)。GPLv2 の内容さえ守ってくれれば、煮たり焼いたり使い易いように改造したりご自由にどうぞ。たぶん gcc + glibc の組合せでしかコンパイルできません。

-bdiff.cpp-

/*
  Explain: Binary Comparing Program
  Version: 1.0.1
  License: GPLv2 or later.
  Author : Lugia Kun
*/

#include <iostream>
#include <iomanip>
#include <fstream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <cerrno>
#include <clocale>
#include <libintl.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

using namespace std;

#define _(a) gettext(a)
#define __(a) (string)(a)

class flags {
    unsigned long int      chunk_size;
    unsigned long long int total_size;
    unsigned long long int file_size[2];
    string file_name[2];
  public:
    flags() {
        chunk_size = 0x0l;
        total_size = 0x0l;
        file_size[0] = 0x0l;
        file_size[1] = 0x0l;
        file_name[0] = "";
        file_name[1] = "";
    }
    
    void setChunk(unsigned long int size, unsigned char unit) {
        switch(unit) {
          case 'b':    // bit
            chunk_size = size / 8;
            break;
          case 'k':    // Kbit
            chunk_size = size * (1024 / 8);
            break;
          case 'm':    // Mbit
            chunk_size = size * (1024 * 1024 / 8);
            break;
          case 'g':    // Gbit
            chunk_size = size * (1024 * 1024 * 1024 / 8);
            break;
          case 'B':    // Byte
            chunk_size = size;
            break;
          case 'K':    // KByte
            chunk_size = size * 1024;
            break;
          case 'M':    // MByte
            chunk_size = size * 1024 * 1024;
            break;
          case 'G':    // GByte
            chunk_size = size * 1024 * 1024 * 1024;
            break;
          default:     // Default is Byte.
            chunk_size = size;
            break;
        }
    }
    
    void setTotal(unsigned long long int size, unsigned char unit) {
        switch(unit) {
          case 'b':    // bit
            total_size = size / 8;
            break;
          case 'k':    // Kbit
            total_size = size * (1024 / 8);
            break;
          case 'm':    // Mbit
            total_size = size * (1024 * 1024 / 8);
            break;
          case 'g':    // Gbit
            total_size = size * (1024 * 1024 * 1024 / 8);
            break;
          case 'B':    // Byte
            total_size = size;
            break;
          case 'K':    // KByte
            total_size = size * 1024;
            break;
          case 'M':    // MByte
            total_size = size * 1024 * 1024;
            break;
          case 'G':    // GByte
            total_size = size * 1024 * 1024 * 1024;
            break;
          default:     // Default is Byte.
            total_size = size;
            break;
        }
    }
    
    unsigned long int getChunkSize(void) {
        return chunk_size;
    }
    
    unsigned long long int getTotalSize(void) {
        return total_size;
    }
    
    void setFile(int id, string filename) {
        if(id >= 2) {
            throw __("System Error: Invalid Argument : flags::getFileSize()");
        }
        struct stat dt;
        stat(filename.c_str(), &dt);
        if(errno != 0) {
            throw filename + ": " + strerror(errno);
        }
        file_size[id] = dt.st_size;
        file_name[id] = filename;
    }
    
    unsigned long long int getFileSize(unsigned int id) {
        if(id >= 2) {
            throw __("System Error: Invalid Argument : flags::getFileSize()");
        }
        return file_size[id];
    }
    
    string getFileName(unsigned int id) {
        if(id >= 2) {
            throw __("System Error: Invalid Argument : flags::getFileName()");
        }
        return file_name[id];
    }
};

ifstream ifile[2];

int main(int argc, char** argv) {
  try {
//    setlocale(LC_ALL, "");
    locale::global(locale(""));
    cout.imbue(locale("C"));
    cerr.imbue(locale(""));
    clog.imbue(locale(""));
    cin.imbue(locale(""));
    
    errno = 0;
    flags flgs;
    char* ret = NULL;
    unsigned long long int size = 0;
    unsigned char          unit = '\0';
    int c = 0;
    for(int i = 1; i < argc; i++) {
        if(*argv[i] == '-') {
            switch(*(argv[i] + 1)) {
              case 'c':
              case 'C':
                size = strtol(argv[i] + 2, &ret, 10);
                unit = *ret;
                if(i + 1 < argc && (size == 0 || ret == argv[i] + 2)) {
                    size = strtol(argv[i + 1], &ret, 10);
                    unit = *ret;
                    if(size == 0 || ret == argv[i + 1]) {
                        throw __("Chunk Size is not specified.");
                    } else {
                        i++;
                    }
                }
                flgs.setChunk(size, unit);
                break;
              case 't':
              case 'T':
                size = strtoll(argv[i] + 2, &ret, 10);
                unit = *ret;
                if(i + 1 < argc && (size == 0 || ret == argv[i] + 2)) {
                    size = strtoll(argv[i + 1], &ret, 10);
                    unit = *ret;
                    if(size == 0 || ret == argv[i + 1]) {
                        throw __("Total Size is not specified.");
                    } else {
                        i++;
                    }
                }
                flgs.setTotal(size, unit);
                break;
              default:
                throw (string) _("Unknown Option:") + " \'" + *(argv[i] + 1) + "\'";
                break;
            }
        } else {
            if(c >= 2) {
                throw __("Too many files specified.");
            }
            flgs.setFile(c, argv[i]);
            if(errno != 0) {
                throw (string) argv[i] + ": " + strerror(errno);
            }
            c++;
        }
    }
    if(c < 2) {
        throw flgs;
    }
    if(flgs.getChunkSize() == 0) {
        flgs.setChunk(16, 'K');    // 16kByte
    }
    if(flgs.getTotalSize() == 0 
      || flgs.getFileSize(0) < flgs.getTotalSize() 
      || flgs.getFileSize(1) < flgs.getTotalSize()) {
        if(flgs.getFileSize(0) > flgs.getFileSize(1)) {
            flgs.setTotal(flgs.getFileSize(1), 'B');
        } else {
            flgs.setTotal(flgs.getFileSize(0), 'B');
        }
    }
    clog << _("Compare Detail is ...") << endl;
    clog << _("Chunk Size    : ") << setw(26) << flgs.getChunkSize() << _(" Byte(s)") << endl;
    clog << _("Total Size    : ") << setw(26) << flgs.getTotalSize() << _(" Byte(s)") << endl;
    clog << _("File 1 (Size) : ") << left << setw(26) << flgs.getFileName(0);
    clog << " (" << flgs.getFileSize(0) << ")" << endl;
    clog << _("File 2 (Size) : ") << left << setw(26) << flgs.getFileName(1);
    clog << " (" << flgs.getFileSize(1) << ")" << endl;
    clog << _("Verifing may takes very long time. Are you sure? (y/n): ");
    char rep;
    cin >> rep;
    if(rep == 'n' || rep == 'N') {
        throw __("Verifing is canceled by user.");
    }
    if(errno != 0) {
        throw errno;
    }
        
    char* tmp[2];
    for(int i = 0; i < 2; i++) {
        ifile[i].open(flgs.getFileName(i).c_str(), ifstream::binary);
        if(ifile[i].bad() || errno != 0) {
            throw flgs.getFileName(i) + ": " + strerror(errno);
        }
        tmp[i] = new char[flgs.getChunkSize()];
    }
    
    for(unsigned long long int i = 0; i < flgs.getTotalSize(); i += flgs.getChunkSize()) {
        for(int j = 0; j < 2; j++) {
            if(ifile[j].eof() == true) {
                memset(tmp[j], 0x0, flgs.getChunkSize());
            } else {
                ifile[j].read(tmp[j], flgs.getChunkSize());
                if(ifile[j].bad() || errno != 0) {
                    throw flgs.getFileName(j) + ": " + strerror(errno);
                }
                ifile[j].seekg(flgs.getChunkSize(), ifstream::cur);
                if(ifile[j].bad() || errno != 0) {
                    throw flgs.getFileName(j) + ": " + strerror(errno);
                }
            }
        }
        for(unsigned long int j = 0; j < flgs.getChunkSize(); j += 16) {
            if(i + j >= flgs.getTotalSize())
                break;
            
            bool check = false;
            for(int k = 0; k < 16; k++) {
                if(j + k < flgs.getChunkSize() && 
                  (*(tmp[0] + j + k) != *(tmp[1] + j + k))) {
                    check = true;
                    break;
                }
            }
            if(check) {
                for(int l = 0; l < 2; l++) {
                    cout << ((l == 0) ? "-" : "+");
                    cout.fill('0');
                    cout << setw(16) << hex << i + j << "  ";
                    for(int k = 0; k < 16 && j + k < flgs.getChunkSize(); k++) {
                        cout.fill('0');
                        cout << setw(2) << ((int)*(tmp[l] + j + k) & 0x000000ff) << " ";
                    }
                    cout << endl;
                }
            }
        }
    }
    
    for(int i = 0; i < 2; i++) {
        ifile[i].close();
        delete [] tmp[i];
    }
    return 0;
  }
  catch(bad_alloc) {
    cerr << argv[0] << ": " << strerror(ENOMEM) << endl;
    return ENOMEM;
  }
  catch(int err) {
    cerr << argv[0] << ": " << strerror(err) << endl;
    return err;
  }
  catch(string str) {
    cerr << argv[0] << ": " << _(str.c_str()) << endl;
    return 0300;
  }
  catch(...) {
    cerr << __("Usage: ") << argv[0] << __(" [options] file1 file2") << endl;
    return 0400;
  }
}

変更履歴

バージョン 変更内容
1.0.0 初版
1.0.1 メモリ不足の場合のエラーメッセージを変更。"-c 4g"のオーバーフローエラーを修正。

*1:Eはエクサ。1EByte はおよそ[tex:1.8 \times 10^{19}]バイト。