`magic_match_io` の正規表現マッチングに `seek` を統合してコードを一本化
magic_match_io メソッドに正規表現マッチングのロジックを統合し、独立していた match_regex メソッドを削除しました。これにより、オフセット指定のある正規表現マッチングが正しく動作するようになり、コードの重複も解消されています。
背景
リファクタリング前の実装では、正規表現・範囲オフセット・バイト文字列の3種類の値に対するマッチング処理が統一されていませんでした。特に正規表現の処理は match_regex という独立したメソッドに切り出されており、magic_match_io から呼び出される設計でした。
match_regex の実装には問題がありました。オフセットに Range が指定されている場合、offset.begin の位置まで読み飛ばしてから 256 バイトを読むという動作になっていましたが、バイト文字列マッチングや範囲オフセットのパターンで採用されている io_seek を使わず、io.read(start, buffer) で読み捨てる方式をとっていました。この不統一が、範囲オフセット付き正規表現マッチングの処理を magic_match_io の統一的なフローから切り離す原因になっていました。
技術的な変更
match_regex メソッドを廃止し、そのロジックを magic_match_io 内に統合しました。変更のポイントは、値の種別(is_range・is_regexp)とサンプルサイズの計算を先に行い、IO 読み取りとマッチング判定を分離した構造にある点です。
変更前:
def self.magic_match_io(io, matches, buffer)
matches.any? do |offset, value, children|
match =
if value
if value.is_a?(Regexp)
match_regex(io, offset, value, buffer)
elsif Range === offset
io_seek(io, offset.begin, buffer)
x = io.read(offset.end - offset.begin + value.bytesize, buffer)
x && x.include?(value)
else
io_seek(io, offset, buffer)
io.read(value.bytesize, buffer) == value
end
end
io.rewind
match && (!children || magic_match_io(io, children, buffer))
end
end
def self.match_regex(io, offset, regexp, buffer)
start = offset.is_a?(Range) ? offset.begin : offset
io.read(start, buffer) if start > 0
data = io.read(256, buffer)
return false unless data
data.match?(regexp)
end
変更後:
def self.magic_match_io(io, matches, buffer)
matches.any? do |offset, value, children|
match = if value
is_range = Range === offset
is_regexp = Regexp === value
sample_size = is_regexp ? 256 : value.bytesize
x = if is_range
io_seek(io, offset.begin, buffer)
io.read(offset.end - offset.begin + sample_size, buffer)
else
io_seek(io, offset, buffer)
io.read(sample_size, buffer)
end
if is_regexp
x&.match?(value)
elsif is_range
x&.include?(value)
else
x == value
end
end
io.rewind
match && (!children || magic_match_io(io, children, buffer))
end
end
旧実装では正規表現かつ範囲オフセットのケースで match_regex が io.read(start, buffer) を使って読み捨てていましたが、新実装では他のケースと同様に io_seek を使って位置を合わせるようになりました。また、nil チェックが return false unless data から x&.match?(value) / x&.include?(value) へと safe navigation operator を使う形に統一されています。
テストも同様に更新され、match_regex を直接呼ぶコードが magic_match_io 経由の呼び出しに置き換えられました。これにより、パブリックに近いインターフェースを通じた検証になっています。
設計判断
IO 読み取りとマッチング判定の分離という構造が採用されました。変更後のコードは「どこから何バイト読むか」の決定と「読んだデータをどう比較するか」の決定を明確に分けており、値の種別ではなくオフセットの種別(is_range)がバッファ読み取りの形を決め、値の種別(is_regexp)がマッチング方式を決める構造になっています。
サンプルサイズの計算も統一されました。正規表現の場合は固定の 256 バイト、バイト文字列の場合は value.bytesize という計算ロジックが sample_size 変数に集約され、読み取り処理の重複が排除されています。
match_regex の削除はプライベートメソッドの整理に留まりますが、テストが内部メソッドを直接呼ぶ形から magic_match_io を経由する形になったことで、実装の詳細への依存が減っています。
まとめ
本PRは、正規表現マッチングを他のパターンと同一のシーク方式に揃えながら match_regex を削除した変更です。IO 操作の統一と nil 安全性の向上、さらにテストの内部依存解消を一度に達成しており、コードの見通しと正確性の両面が改善されています。