何で?
ん~~
ハッシュコードが欲しかったんです。
でも、MD5とかSHA1とか、そこまで長いのはちょっとねー
もっと手軽なハッシュコード…
あー、CRC!
って訳
一応断っておくと、C#のGetHashCode()
はダメっすよ。
物凄く偏ったコードを返すので。
参考資料
目指すところ
-
wikipediaにある様に、諸々の形式があるのでそこら辺を網羅したいなぁ
8・16・32ビットのCRCを計算できる様にね -
しかも
CRC
クラス一発でそいつらを使い分けられると嬉しいかな -
右送りとか左送りとかも何のことやら
-
InitialマスクとかFinalマスクとかも選択可
-
で、忘れてはならないのは、CRCの検算ですね
と云うのも、検算できないと自分の実装が正しいか分からないじゃないですか -
実装はkadzusさんとこのを踏襲して、
HashAlgorithm
のサブクラスとします! -
検算系は七誌さんの資料を参考に手探ってみました!
実装~
えーと、多項式に詳しい訳でもないので解説は他の方にお願いせざるを得ません…
とか思ったら、もうソースを載せるくらいしかないですね。
てな訳で、まずは定義部分
public enum CRCbitFeed {
Left,
Right,
}
public enum CRCpolynomial : uint {
CRC8 = 1 << 7 | 1 << 6 | 1 << 4 | 1 << 2 | 1,
CRC8_ATM = 1 << 2 | 1 << 1 | 1,
CRC8_CCITT = 1 << 7 | 1 << 3 | 1 << 2 | 1 << 1 | 1,
CRC8_Dallas = 1 << 5 | 1 << 4 | 1 << 1 | 1,
CRC8_SEA = 1 << 4 | 1 << 3 | 1 << 2 | 1,
CRC16 = 1 << 15 | 1 << 2 | 1,
CRC16_CCITT = 1 << 12 | 1 << 5 | 1,
CRC16_IBM = CRC16,
CRC32 = 1 << 26 | 1 << 23 | 1 << 22 | 1 << 16 | 1 << 12 | 1 << 11 | 1 << 10 | 1 << 8 | 1 << 7 | 1 << 5 | 1 << 4 | 1 << 2 | 1 << 1 | 1,
CRC32_C = 1 << 28 | 1 << 27 | 1 << 26 | 1 << 25 | 1 << 23 | 1 << 22 | 1 << 20 | 1 << 19 | 1 << 18 | 1 << 14 | 1 << 13 | 1 << 11 | 1 << 10 | 1 << 9 | 1 << 8 | 1 << 6 | 1,
CRC32_K = 1 << 30 | 1 << 29 | 1 << 28 | 1 << 26 | 1 << 20 | 1 << 19 | 1 << 17 | 1 << 16 | 1 << 15 | 1 << 11 | 1 << 10 | 1 << 7 | 1 << 6 | 1 << 4 | 1 << 2 | 1 << 1 | 1,
}
CRCpolynomial
の定義が、wikipedia
の主な標準CRCに対応している事が分かるかと思います。
で、これをこんな形で使えると良いかなぁ、と…
using(CRC crc = new CRC(CRCpolynomial.CRC32)) {
byte[] c = crc.ComputeHash(Encoding.Default.GetBytes("abcd"));
Console.WriteLine(BitConverter.ToString(c));
}
そんなこんなでこうなりました。
ざっくり説明すると—
- 割り当てられた多項式をもとにして、
CRC
のサイズを自動判定して変換テーブルを作っちゃう仕様です - その関係で、ビット送り方向もコンストラクタに与える必要があります
省略時は右送りとなります -
InitialMask
とFinalMask
はインスタンス化した後で変更できます
取り敢えず既定値は両方ともtrue
です -
InitialValue
は実装しませんでした - 検算メソッドについても下のソースに入っていますが、後でちょっと解説しようと思います
public class CRC : HashAlgorithm {
public CRC(CRCpolynomial poly, CRCbitFeed feed = CRCbitFeed.Right) {
InitialMask = true;
FinalMask = true;
Polynomial = (uint)poly;
BitFeed = feed;
base.HashSizeValue = (int)Math.Floor(Math.Log(Polynomial) / Math.Log(2F) / 8 + 1) * 8;
CRCmask = (uint)Math.Pow(2, base.HashSizeValue) - 1;
CRCtable = new uint[256];
if(BitFeed == CRCbitFeed.Left) {
CRCpoly = Polynomial;
uint msb = (uint)(1 << (base.HashSizeValue - 1));
for(uint i = 0; i < 256; i++) {
uint c = i << (base.HashSizeValue - 8);
for(int j = 0; j < 8; j++)
c = (uint)((c << 1) ^ (((c & msb) != 0) ? CRCpoly : 0));
CRCtable[i] = c & CRCmask;
}
} else {
for(int i = 0; i < base.HashSizeValue; i += 4)
CRCpoly = (CRCpoly << 4) | Rtbl[(Polynomial >> i) & 0xf];
for(uint i = 0; i < 256; i++) {
uint c = i;
for(int j = 0; j < 8; j++)
c = (c & 1) != 0 ? (CRCpoly ^ (c >> 1)) : (c >> 1);
CRCtable[i] = c;
}
}
Initialize();
}
uint[] Rtbl = { 0x0, 0x8, 0x4, 0xc, 0x2, 0xa, 0x6, 0xe, 0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf };
bool _InitialMask = false;
public bool InitialMask {
get {
return _InitialMask;
}
set {
_InitialMask = value;
Initialize();
}
}
public bool FinalMask { get; set; }
public uint Polynomial { get; private set; }
public CRCbitFeed BitFeed { get; private set; }
public int CRCsize {
get { return base.HashSizeValue / 8; }
}
byte[] CRCcrc;
uint LSTvalue;
uint CRCmask;
uint CRCvalue;
uint CRCpoly;
uint[] CRCtable;
protected override void HashCore(byte[] array, int ibStart, int cbSize) {
unchecked {
while(--cbSize >= 0)
CRCvalue = CRCtable[(CRCvalue ^ array[ibStart++]) & 0xff] ^ (CRCvalue >> 8);
}
LSTvalue = CRCvalue;
}
protected override byte[] HashFinal() {
HashValue = new byte[CRCsize];
for(uint i = (uint)HashValue.Length, temp = CRCvalue ^ (FinalMask ? CRCmask : 0); i > 0; temp >>= 8)
HashValue[--i] = (byte)(temp & 0xff);
return HashValue;
}
public override void Initialize() {
CRCvalue = InitialMask ? CRCmask : 0;
}
public bool ConsistencyCheck(byte[] crc, byte[] buffer) {
CRCcrc = crc.Reverse().ToArray();
uint CRCuint = 0;
CRCvalue = CRCmask;
ComputeHash(buffer);
CRCuint = LSTvalue;
CRCvalue = CRCuint;
HashCore(CRCcrc, 0, CRCcrc.Length);
if(CRCvalue == 0)
return true;
CRCcrc = CRCcrc.Select(e => (byte)(e ^ 0xff)).ToArray();
CRCvalue = CRCuint;
HashCore(CRCcrc, 0, CRCcrc.Length);
if(CRCvalue == 0)
return true;
CRCvalue = 0;
ComputeHash(buffer);
CRCuint = LSTvalue;
CRCvalue = CRCuint;
HashCore(CRCcrc, 0, CRCcrc.Length);
if(CRCvalue == 0)
return true;
CRCcrc = CRCcrc.Select(e => (byte)(e ^ 0xff)).ToArray();
CRCvalue = CRCuint;
HashCore(CRCcrc, 0, CRCcrc.Length);
if(CRCvalue == 0)
return true;
return false;
}
}
で使い方。
byte[] s = Encoding.Default.GetBytes("abcd");
byte[] c;
using(CRC crc = new CRC(CRCpolynomial.CRC32)) {
crc.InitialMask = true;
crc.FinalMask = true;
c = crc.ComputeHash(s);
Console.WriteLine("CRC BitFeed\t= {0}", crc.BitFeed);
Console.WriteLine("InitialMask\t= {0}", crc.InitialMask);
Console.WriteLine("FinalMask\t= {0}", crc.FinalMask);
Console.WriteLine();
Console.WriteLine(BitConverter.ToString(c));
}
using(CRC crc = new CRC(CRCpolynomial.CRC32)) {
crc.InitialMask = true;
crc.FinalMask = true;
Console.WriteLine();
Console.WriteLine("CRC Check\t: {0}", crc.ConsistencyCheck(c, s));
}
実行すると、こんな感じ。
実は、最初のusing
内でマスクの値をどう変えても、検算メソッドはきちんと判断してくれます。
と云うか、そういう風に作ってみました。
検算!
元データにCRCを合体させた全体でCRC計算を行うと、一定値になりまっせー
と云う事らしいです。この一定値をマジックナンバーと呼ぶ、と…
ここで気を付ける点は、CRCは内部的にはuint
で表されていたものをバイト反転して配列として返しているので、合体させるときにもう一回反転してあげないといけないところですね。
で、検算!
確かに、21-44-DF-1C
ってなってますね。
using(CRC crc = new CRC(CRCpolynomial.CRC32)) {
crc.InitialMask = true;
crc.FinalMask = true;
c = crc.ComputeHash(s);
Console.WriteLine("CRC BitFeed\t= {0}", crc.BitFeed);
Console.WriteLine("InitialMask\t= {0}", crc.InitialMask);
Console.WriteLine("FinalMask\t= {0}", crc.FinalMask);
Console.WriteLine();
Console.WriteLine(BitConverter.ToString(c));
Console.WriteLine();
byte[] d = crc.ComputeHash(s.Concat(c.Reverse()).ToArray()); <== ここね
Console.WriteLine(BitConverter.ToString(d));
}
でも、このマジックナンバーが一定であるとは限らないすよね。
何ちゃらマスクが二つあるので、少なくとも4パターンのマジックナンバーがある筈。
で、剣山!
何とかマスクは両方ともfalse
にしてみましょう。
using(CRC crc = new CRC(CRCpolynomial.CRC32)) {
crc.InitialMask = false;
crc.FinalMask = false;
c = crc.ComputeHash(s);
Console.WriteLine("CRC BitFeed\t= {0}", crc.BitFeed);
Console.WriteLine("InitialMask\t= {0}", crc.InitialMask);
Console.WriteLine("FinalMask\t= {0}", crc.FinalMask);
Console.WriteLine();
Console.WriteLine(BitConverter.ToString(c));
Console.WriteLine();
byte[] d = crc.ComputeHash(s.Concat(c.Reverse()).ToArray());
Console.WriteLine(BitConverter.ToString(d));
}
おや?
マジックナンバーがゼロだ。
そういえば、どこかの記事でcrc
くっつけて新たなcrc
を計算するとそれはゼロになりまーす、と云う記事を見た記憶もあるなぁ…
じゃぁ、さっきのマジックナンバーはどこから来た?
って、そりゃーマスクの関係でしょう。どう繋がってくるかは良く分からないけど、計算過程が異なるから結果も変わってくるんでしょうね。
じゃ、ここを起点に色々考えてみましょう。
まず手始めにInitialMask
がtrue
でFinalMask
がfalse
の場合。
using(CRC crc = new CRC(CRCpolynomial.CRC32)) {
crc.InitialMask = true;
crc.FinalMask = false;
c = crc.ComputeHash(s);
Console.WriteLine("CRC BitFeed\t= {0}", crc.BitFeed);
Console.WriteLine("InitialMask\t= {0}", crc.InitialMask);
Console.WriteLine("FinalMask\t= {0}", crc.FinalMask);
Console.WriteLine();
Console.WriteLine(BitConverter.ToString(c));
Console.WriteLine();
byte[] d = crc.ComputeHash(s.Concat(c.Reverse()).ToArray());
Console.WriteLine(BitConverter.ToString(d));
}
問題なし、ゼロになりました。
お次はInitialMask
がfalse
でFinalMask
がtrue
の場合。
using(CRC crc = new CRC(CRCpolynomial.CRC32)) {
crc.InitialMask = false;
crc.FinalMask = true;
c = crc.ComputeHash(s);
Console.WriteLine("CRC BitFeed\t= {0}", crc.BitFeed);
Console.WriteLine("InitialMask\t= {0}", crc.InitialMask);
Console.WriteLine("FinalMask\t= {0}", crc.FinalMask);
Console.WriteLine();
Console.WriteLine(BitConverter.ToString(c));
Console.WriteLine();
byte[] d = crc.ComputeHash(s.Concat(c.Reverse()).ToArray());
Console.WriteLine(BitConverter.ToString(d));
}
ありゃ?
所謂マジックナンバー…
んんんー…
お!
crc
がFinalMask
でビット逆転しているので、そいつを元に戻せばいいのでは?
よし、じゃぁ見参!
using(CRC crc = new CRC(CRCpolynomial.CRC32)) {
crc.InitialMask = false;
crc.FinalMask = true;
c = crc.ComputeHash(s);
Console.WriteLine("CRC BitFeed\t= {0}", crc.BitFeed);
Console.WriteLine("InitialMask\t= {0}", crc.InitialMask);
Console.WriteLine("FinalMask\t= {0}", crc.FinalMask);
Console.WriteLine();
Console.WriteLine(BitConverter.ToString(c));
Console.WriteLine();
byte[] d = crc.ComputeHash(s.Concat(c.Reverse().Select(e => (byte)(e ^ 0xff))).ToArray()); <== そいつ
Console.WriteLine(BitConverter.ToString(d));
}
ん?
微妙…
あー、結果の方もFinalMask
でビット反転してるからだ。
じゃぁ、こう。
using(CRC crc = new CRC(CRCpolynomial.CRC32)) {
crc.InitialMask = false;
crc.FinalMask = true;
c = crc.ComputeHash(s);
Console.WriteLine("CRC BitFeed\t= {0}", crc.BitFeed);
Console.WriteLine("InitialMask\t= {0}", crc.InitialMask);
Console.WriteLine("FinalMask\t= {0}", crc.FinalMask);
Console.WriteLine();
Console.WriteLine(BitConverter.ToString(c));
Console.WriteLine();
crc.FinalMask = false; <== こう
byte[] d = crc.ComputeHash(s.Concat(c.Reverse().Select(e => (byte)(e ^ 0xff))).ToArray());
Console.WriteLine(BitConverter.ToString(d));
}
では最後にInitialMask
・FinalMask
ともにtrue
の場合。
using(CRC crc = new CRC(CRCpolynomial.CRC32)) {
crc.InitialMask = true;
crc.FinalMask = true;
c = crc.ComputeHash(s);
Console.WriteLine("CRC BitFeed\t= {0}", crc.BitFeed);
Console.WriteLine("InitialMask\t= {0}", crc.InitialMask);
Console.WriteLine("FinalMask\t= {0}", crc.FinalMask);
Console.WriteLine();
Console.WriteLine(BitConverter.ToString(c));
Console.WriteLine();
crc.FinalMask = false;
byte[] d = crc.ComputeHash(s.Concat(c.Reverse().Select(e => (byte)(e ^ 0xff))).ToArray());
Console.WriteLine(BitConverter.ToString(d));
}
よし!
これで、全パターンの検算結果をゼロで取得することができました。
で、この4パターンの検算を一気に詰め込んだのがConsistencyCheck()
メソッドです。
与えられたcrc値
がどんなマスク指定で作成されたものであっても、全パターンのチェックを若干効率的に行ってくれると云う…
心残り
と、此処にきて思っちゃったんですが、ビット送り方向指定によってもcrc
値は変わるなぁ。
例えば—
crc値
は異なりますね。
でも、検算結果はゼロになっているので実装上の問題はなさそうですが、CRC
クラスをインスタンス化する際のBitFeed
方向の選択を間違えると、正しく判定できないって事になっちゃうかぁ…
そこまでの自動判定を実現させるならCRC
クラスにConsistencyCheck()
を実装するんじゃなくて、CRCconsistencyCheck
クラスにするとかが必要っぽいですね。
と云うような辺りが心残ってしまいました。
最後にCRC
長を変えたパターンのテスト結果を載せて締め括りたいと思います。
よしよし、検算結果もゼロなので、実装に問題はないと云う事で良いですね。
では。