bin2hexとhex2bin 〜vimをバイナリエディタ的に使う、のおまけ〜
前回エントリで軽く触れたbin2hexとhex2bin。
バイナリファイルと16進数ダンプを相互に変換するプログラム。
拡張性とかそういうのはあまり考えていない。
引数はファイルのみ。
そのうち改造しよう。
まぁ、なんだ。Parsecの使い方の覚え書き程度に。
フォーマット
16進数ダンプするにあたって、まずはフォーマットから。
C言語風なコメントでアドレスを、シェルスクリプト風なコメントでASCIIをそれぞれ括り、まんなかに16進数ダンプを入れていきます。
/* 00000000 */ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 #...............
こんな感じ。
なんでC言語風なコメントとシェル風なコメント混ぜてしもたんやろ。
昔の俺に聞かないとと分からないなぁ...。
実際には汎用的(?)にhex2binから先に作って、それからそれにあわせてbin2hexを作ったので、実はこれ以外のフォーマットもいける。
というか、コメント部分は一切無視するので何処に入れても構わないし、1行にいくつデータがあっても構わない。
なので前回エントリでやったvimをバイナリエディタライクにつかう.vimrcを書いてもデータ破損は無いはず。
そもそもデータ破損は、xxdが1行のデータ数を16としているために、例えば
00000000: 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ...............
というxxdフォーマットのデータに対して、ffを挿入すると
00000000: 00 01 02 03 04 05 06 07 ff 08 09 0a 0b 0c 0d 0e 0f ...............
となるが、xxd -rした際に17個目のデータとなる0fが無視されてしまい、再びxxd -g1すると
00000000: 00 01 02 03 04 05 06 07 ff 08 09 0a 0b 0c 0d 0e ...............
となってしまうから、
また、xxdは16進数データとasciiとの区別にスペース二個を使っているために、例えば
00000000: 33 34 20 ff ff ff 34 ...
というデータに対して、ffを消そうとして、dwを押してしまうと
00000000: 33 34 20 ff ff 34 ...
の様に、連続するスペースがすべて消されてしまい、再びxxd -rとxxd -g1をすると
00000000: 33 34 20 ff ff 34 34 ..4
と、データが増えたり、場合によっては最悪それ以降がすべて消えたりしてしまうことによる。
まぁ、そんなこんななので、データ区切りを明白にしつつ、1行にいくつでもデータを許容すれば、データ破損は防げる、ということになる。多分。
あと、個人的に欲しいなと思って実装したのが、"\a"とバックスラッシュ+asciiと書くことで、そのasciiの文字コードを入れることが出来る。
わざわざ文字コード参照するのメンドイしね。
\H \e \l \l \o \, \ /* <- BackSlash + Space */ \W \o \r \l \d 2e # 2e = \.
というデータは"Hello, World."になります。
bin2hex
そんなわけでバイナリから16進数ダンプ。
タダ単にordしてshowHexするだけなんですが、フォーマットのために色々やってます。
かなり実装が汚い...気がする。そのうちgetoptつけて書き直してー。
import Char import System.Environment import Data.List import Numeric readARGV :: IO String readARGV = do args <- getArgs if null args then getContents else mapM readFile args >>= return . concat main :: IO () main = do readARGV >>= putStr . bin2hex nColumns :: Int nColumns = 16 bin2hex :: String -> String bin2hex = unlines . map showLine . zip [0..] . takes nColumns takes :: Int -> [a] -> [[a]] takes _ [] = [] takes n xxs = let (y,ys) = splitAt n xxs in y:(takes n ys) charToHex :: Char -> [Char] charToHex = ([[x,y] | x <- hexChars, y <- hexChars] !!) . ord where hexChars = "0123456789abcdef" padding :: Int -> a -> [a] -> [a] padding n p xs = replicate (n - length xs) p ++ xs replaceUnprintable :: Char -> Char replaceUnprintable c = if isAscii c && isPrint c then c else '.' showLine :: (Int,String) -> String showLine (i,xs) = let halfColumns = nColumns `div` 2 in let (xs1,xs2) = splitAt halfColumns xs in "/* " ++ (padding 8 '0' $ showHex i "0") ++ " */ " ++ (intercalate " " $ take halfColumns $ map charToHex xs1 ++ repeat " ") ++ " " ++ (intercalate " " $ take halfColumns $ map charToHex xs2 ++ repeat " ") ++ " # " ++ (map replaceUnprintable xs)
まぁ、割と短めなんでないでしょうか。
かるーく触れていくと、readARGVはたしかrubyか何かにあったやつからアイデアを拝借。
引数があればそれらをファイルと見なしてread + concat。そーでなければ標準入力から入力。
要はcat的な挙動。
nColumnsは1行に出力するデータ数。ただし16意外にするとバグる予感。(いや、簡単になおるんだけど...)
もとよりハードコーディングでもいいかとか思ってるので特に直す気はない。
takesはwrap...というかなんというか、与えられた幅ずつリストを分割する関数。
ありそうで無いんだよなぁ...。1行に出力するデータ数ずつ入力を区切る。
charToHexは16進数ダンプ。内包表記は後ろの方が「早く」回るのでこの順で生成すると二桁の16進数が作れる。
Numeric.showHexあたりを使えよ、というお話。
paddingはそのままパディング。アドレス部分に使ってる。
桁数とパディングに使う要素を与える。
あー、アドレス8桁超えたら使えねぇや。まぁいいか。
replaceUnprintableはhexdumpとかでよくある、印刷出来ない文字を.で表すための奴。
で、一番のメインがshowLine。
と、言ってもあまりたいしたことない、というか、汚い実装だなぁ...。
" "をrepeatさせて後ろにくっつけて、takeすることで、ascii表示部分をそろえています。
そんくらい。
無駄も含めて45行程度でhexdumpもどき完成。
なんというかこの辺の美しさというか表現力が関数型の魅力な気がする。
まぁ、Cで書いてもそんな行数変わらないと思うけど。
hex2bin
こちらは16進数からバイナリへ。
パースがあるので先のと比べると多少難易度あがる、のかな?
Parsecを使っていきます。今回の場合かなり単純なフォーマットだからチューリングマシン的に先頭からフラグ変えつつ読んだほうが早いやも。
import Text.ParserCombinators.Parsec as Parsec import Char import Maybe import System readARGV :: IO String readARGV = do args <- getArgs if null args then getContents else mapM readFile args >>= return . concat main :: IO () main = do readARGV >>= putStr . hex2bin hex2bin :: String -> String hex2bin str = case parse parseIt "" str of Left e -> error $ show e Right xs -> map chr $ xs parseIt :: GenParser Char () [Int] parseIt = do (many $ parseComment <|> parseByte) >>= return . catMaybes parseComment :: GenParser Char () (Maybe Int) parseComment = (parseSpaces <|> parseLineComment <|> parseMultiComment) >> return Nothing where parseSpaces = do many1 space parseLineComment = do char '#' many $ noneOf ['\n'] parseMultiComment = do string "/*" manyTill anyChar $ try $ string "*/" parseByte :: GenParser Char () (Maybe Int) parseByte = do parseDigit <|> parseRaw where parseDigit = do count 2 hexDigit >>= return . Just . read . ("0x"++) parseRaw = do char '\\' anyChar >>= return . Just . ord
こちらも46行程度。
今見るとびみょーに何やってるの?的な部分がいくつか...。なんでMaybe Intやの?
最初にskipMany parseCommentして、parseByteの最後にも同様にskipMany parseCommentするだけでMaybe消せる気がするが...。まぁいいや。
Parsecそのままなんであまり説明する場所無し。Parsecは使いこなせるようになるとかなり便利ですにゃぁ。
まだ使いこなせてないけど。
16進数ダンプのvim syntax
おまけです。
" Vim syntax file " Language: hex file " Last Change: 2009/08/09 23:50:33. " Author: goth_wrist_cut " URL: http://d.hatena.jp/goth_wrist_cut/ " Address := [0-9a-fA-F]\+ " Data := [0-9a-fA-F]\{2\} " Ascii := .* " Line := /* Address */ Data* # Ascii if exists( "b:current_syntax" ) finish endif syntax match gothicHexAddress "/\* *[0-9a-fA-F]\+ *\*/" contains=gothicHexStartAddr,gothicHexEndAddr syntax match gothicHexStartAddr contained "/\*" syntax match gothicHexEndAddr contained "\*/" syntax match gothicHexAscii "#.*" contains=gothicHexStartAscii syntax match gothicHexStartAscii contained "#" highlight link gothicHexAddress Constant highlight link gothicHexStartAddr Identifier highlight link gothicHexEndAddr Identifier highlight link gothicHexAscii Statement highlight link gothicHexStartAscii Identifier let b:current_syntax = "gothicHex"
bin2hexとhex2binとをそれぞれコンパイルしてPATHの通った所に置き、上のsyntaxをgothicHex.vim(仮称)という名前で、~/.vim/syntax/以下に置く。
んで、.vimrcに
augroup Binary autocmd! autocmd BufReadPre *.bin let &binary = 1 autocmd BufReadPost * call BinReadPost() autocmd BufWritePre * call BinWritePre() autocmd BufWritePost * call BinWritePost() autocmd CursorHold * call BinReHex() function! BinReadPost() if &binary silent %!bin2hex set ft=gothicHex endif endfunction function! BinWritePre() if &binary let s:saved_pos = getpos( '.' ) silent %!hex2bin endif endfunction function! BinWritePost() if &binary silent %!bin2hex call setpos( '.', s:saved_pos ) set nomod endif endfunction function! BinReHex() if &binary let s:saved_pos = getpos( '.' ) let s:modified = &modified silent %!hex2bin silent %!bin2hex call setpos( '.', s:saved_pos ) let &modified = s:modified endif endfunction augroup END