Go言語はちょっとしたツールを作るのに適しています。今回は、ブラウザからも使えるバイナリビューワーを作ってみましょう。ブラウザから使えると、よりツールが手軽に使えます。そこで、バイナリファイルの扱いに加えて、ツールをブラウザ対応する方法を紹介します。

バイナリビューワーとは?

バイナリ(英語:binary)とは二進数法を指す言葉ですが、コンピューターが処理するデータを指してバイナリと呼びます。私たちがコンピューターを介して、利用するいろいろなファイルは全て様々な数値の連続で成り立っています。そして、バイナリビューワー(またはエディター)とは、そうした様々なファイルを数値(一般的には16進数)で確認(編集)できるツールを言います。

バイナリを確認すると普段利用しているファイルが、どのようなデータで成り立っているのかを見ることができます。ただし、実際にいろいろなファイルを見てみると分かるのですが、何の知識もなしに見ると、意味不明なデータが並んでいるだけに見えます。しかし、詳しくファイル形式を調べてみると、そうしたデータにどんな意味があるのかが分かります。

Go言語はバイナリファイルの取り扱いも得意なので、こうしたバイナリビューワーの作成も用意です。最初にコマンドラインでツールを作り、その後でブラウザ対応してみましょう。

Go言語でバイナリを処理する方法

最初に、ファイルを読み込んで、バイナリデータを連続で出力するプログラムを作ってみましょう。以下はテキストファイル「test.txt」を1バイトずつ読み込んで画面に表示するプログラムです。

package main

import (
    "fmt"
    "os"
)

// ファイルを読み込んで内容を表示する
func PrintFile(filename string) {
    // ファイルを開く --- (*1)
    fp, err := os.Open(filename)
    if err != nil {
        panic(err)
    }
    // 読み込みバッファを用意する --- (*2)
    buf := make([]byte, 1)
    // 繰り返し表示する --- (*3)
    for {
        // 1バイト読み込む --- (*4)
        cnt, _ := fp.Read(buf)
        if cnt == 0 {
            break
        }
        // 画面に表示する --- (*5)
        fmt.Printf("[%d]", buf[0])
    }
}

func main() {
    PrintFile("test.txt")
    fmt.Printf("\n")
}

上記プログラムを「printfile.go」という名前で保存します。そして、例えば次のようなテキストファイル「test.txt」を用意します。

Keep on asking, and it will be given you;
keep on seeking, and you will find;
keep on knocking, and it will be opened to you;

そして、コマンドラインで以下のようなコマンドを実行します。

go run printfile.go

すると、コマンドラインにファイルの内容が一バイトずつ表示されます。

  • ファイルを読み込んで1バイトずつ出力するプログラム

    ファイルを読み込んで1バイトずつ出力するプログラム

プログラムを確認してみましょう。(*1)の部分でファイルを開いて操作可能な状態にします。そして、(*2)の部分でファイルの内容を読み込むメモリをバッファ(一時記憶領域)として用意します。そして、(*3)のfor構文を用いて繰り返しファイルの内容を読み込みます。その際、(*4)の部分でバッファに1バイト読み込み、(*5)の部分で画面に表示します。

読みやすく揃えて表示しよう

ただし、バイト列が連続で表示されただけでは読みづらく、バイナリビューワーとしては不便です。バイナリビューワーを使う場面というのは、ファイル解析をすることが多いので、これでは不便です。そこで、読みやすくバイナリ列を揃えて表示してみましょう。また10進数ではなく、16進数に直して表示するようにしてみましょう。

以下のプログラムを「printfile2.go」という名前で保存しましょう。

package main
import (
    "fmt"
    "io/ioutil"
)
// ファイルを読み込んで内容を表示する
func PrintFile(filename string) {
    // ファイルの内容を全部読み --- (*1)
    bytes, err := ioutil.ReadFile(filename)
    if err != nil {
        panic(err)
    }
    // 繰り返し表示する --- (*2)
    line := ""
    aline := ""
    for i := 0; i < len(bytes); i++ {
        b := bytes[i]
        // アスキー文字の表示用
        c := string(b)
        if b < 32 || b > 126 { c = "_" }
        aline += c
        // 画面に表示する --- (*3)
        m := i % 16
        if m == 0 { // アドレスを追加
            line += fmt.Sprintf("%04d: ", i)
        }
        line += fmt.Sprintf("%02x", b)
        switch m {
            case 7: // 見やすく区切り線
                line += "|"
            case 15: // 区切り線とアスキー文字
                fmt.Println(line + "|" + aline)
                line = ""
                aline = ""
            default:
                line += " "
        }
    }
    // 表示残しを出力 --- (*4)
    if line != "" {
        fmt.Printf("%-53s|%s\n", line, aline)
    }
}

func main() {
    PrintFile("test.txt")
}

続いて、以下のコマンドを実行すると、見やすく16バイトごとに数値を区切って表示します。

go run printfile2.go
  • 16バイトごとに数値を区切って表示したところ

    16バイトごとに数値を区切って表示したところ

プログラムを確認してみましょう。(*1)の部分ではファイルの内容を読み込みます。一つ前のプログラムでは、一バイトずつ読み込むようなプログラムを作りましたが、ここでは、ioutil.ReadFileを利用してファイルの内容を全部読み込みます。(*2)の部分では繰り返し表示します。(*3)と(*4)の部分では何バイト目かを調べて、区切り線や参考のアスキー文字を出力します。

ブラウザで使えるようにしよう

最後に、これをコマンドラインではなくブラウザから使えるようにしてみましょう。以下のプログラムを「printfile-server.go」という名前で保存します。

package main
import (
    "fmt"
    "io/ioutil"
    "net/http"
)
// サーバーを起動 --- (*1)
func main() {
    http.HandleFunc("/", indexHandler)
    http.HandleFunc("/upload", uploadHandler)
    http.ListenAndServe(":8888", nil)
}
// アクセスがあった時、アップロードフォームを返す --- (*2)
func indexHandler(w http.ResponseWriter, r *http.Request) {
    s := "<html><body>" + 
        "<h1>ファイルを指定してください</h1>" +
        "<form action='/upload' method='post' " +
        " enctype='multipart/form-data'>" +
        "<input type='file' name='upfile'>" +
        "<input type='submit' value='アップロード'>" +
        "</form></body></html>";
    w.Write([]byte(s))
}
// ファイルを投稿した時 --- (*3)
func uploadHandler(w http.ResponseWriter, r *http.Request) {
    file, _, err := r.FormFile("upfile") // ファイルを取得
    if err != nil {
        w.Write([]byte("アップロードエラー"))
        return
    }
    data, err := ioutil.ReadAll(file) // ファイルを読み出す
    if err != nil {
        w.Write([]byte("アップロードエラー"))
        return
    }
    s := getBinStr(data) // データをバイナリ表示 
    w.Write([]byte("<html><body>" + s + 
        "</body></html>"))
}

// バイナリ表示 --- (*4)
func getBinStr(bytes []byte) string {
    // 繰り返し表示する
    result := "<style>" +
        "th { background-color: #f0f0f0; } " +
        ".c { background-color: #fff0f0; } " +
        "td { border-bottom: 1px solid silver } " +
        "</style><table>"
    line := "<tr>"
    aline := ""
    cnt := 2
    for i := 0; i < len(bytes); i++ {
        b := bytes[i]
        // アスキー文字の表示用
        c := string(b)
        if b < 32 || b > 126 { c = "_" }
        if c == ">" { c = "&gt;" }
        if c == "<" { c = "&lt;" }
        aline += c
        // 画面に表示する
        m := i % 16
        if m == 0 { // アドレスを追加
            line += fmt.Sprintf("<th>%04d:</th><td>", i)
        }
        line += fmt.Sprintf("%02x", b)
        switch m {
            case 3, 7, 11: // 見やすく区切り線
                line += "</td><td>"
                cnt -= 1
            case 15: // 区切り線とアスキー文字
                result += line + "</td>"
                result += "<td class='c'>" + aline + "</td></tr>\n"
                line = "<tr>"
                aline = ""
                cnt = 2
            default:
                line += " "
        }
    }
    // 表示残しを出力
    if line != "" {
        result += line
        for j := 0; j < cnt; j++ {
            result += "</td><td>"
        }
        result += "</td><td class='c'>" + aline + "</td></tr>"
    }
    result += "</table>"
    return result
}

そして、コマンドラインで「go run printfile-server.go」と実行します。すると、Go言語でサーバーが起動します。続けて、Webブラウザで「http://localhost:8888」へアクセスします。そして、ファイルを選択して「アップロード」ボタンをクリックすると、バイナリ表示でファイルの内容が表示されます。

  • ファイルをアップロードするとファイルの内容をバイナリ表示する

    ファイルをアップロードするとファイルの内容をバイナリ表示する

プログラムを見てみましょう。(*1)の部分では、Go言語でWebサーバーを起動します。ここでは、localhostのポート8888番でサーバーを起動します。続いて、(*2)の部分では、ブラウザからルートにアクセスがあった時の処理を記述します。ここでは、ファイルのアップロードフォームのHTMLを返します。(*3)の部分では、ファイルをアップロードした時の処理を記述します。アップロードされたファイルの内容を取得して、getBinStr関数を呼び出してバイナリ表示の内容を得てブラウザの画面に書き出します。このgetBinStr関数の内容は、一つ前のプログラムのPrintFile関数とほとんど同じで、テキストではなくHTMLのタグを加えて文字列として返すように修正したものです。

なお、Webサーバーに関しては、本連載の3回目で詳しく紹介していますので、参考にしてみてください。

まとめ

以上、今回は、ブラウザから使えるバイナリビューワーを作ってみました。様々なファイルをバイナリ表示してみると面白いことでしょう。また、こうしたちょっとしたツールをブラウザから使えるようにするなら、より手軽に使ってもらえることも分かることでしょう。本稿がバイナリデータの扱いや、ツールのブラウザ対応の参考になれば幸いです。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ、2010年 OSS貢献者章受賞。技術書も多く執筆している。