はじめに
ZIPを用いた実行ファイルへのリソースの埋め込み方法があることを紹介し、実際にGoの archive/zip
と zip
コマンドと cat
コマンド(と確認のために unzip
コマンド)を用いたリソース埋め込みの例を解説する。
Goのリソース埋め込み
Goでアプリケーションを書く際、CSSやJavaScript、画像、テンプレートといったリソースは実行ファイルとは別に配置するか実行ファイルの中に埋め込むかしないといけない。リソースの埋め込みにはコードジェネレータを用いる方法とZIPを使う方法がある。
コードジェネレータ
リソースの埋め込みでよく紹介されるのはコードジェネレータを用いてリソースをGoのソースコードに変換するという方法で、実際 Awesome Go の Resource Embeddingの項目 にあるのはコードジェネレータを用いてGoのソースコードに変換するアプローチのものしかない。
ZIP
一方、リソースをZIPファイルにまとめて実行ファイルに追記するという埋め込み方もある。既に Zgok というライブラリがあり、作者の @srtkkou さんが解説(Golangで静的ファイルをバイナリに含めるライブラリを書いてみた)を書かれている。
ZIPファイルの仕様ではself-extractingなZIPファイルはターゲットプラットフォームごとの展開コードを含まなければならないとある。
4.1.9 ZIP files MAY be streamed, split into segments (on fixed or on
removable media) or "self-extracting". Self-extracting ZIP
files MUST include extraction code for a target platform within
the ZIP file.
また、zip
コマンドのmanページにはself-extractingな実行可能ファイル兼アーカイブは既存のアーカイブにSFXスタブを前置することで作られるとある。
A self-extracting executable archive is created by prepending the SFX stub to an existing archive.
これらのことから、実行ファイルそれ自体をZIPファイルとして取り扱うGoのコマンドを書き、その実行ファイルの末尾にZIPファイルを足せばリソースを埋め込めることがわかる。1
ZIPでのリソース埋め込みの例
ここでは main.go
と assets/templates/hello.tmpl
だけからなるシンプルな例について考える。
$ tree .
.
├── assets
│ └── templates
│ └── hello.tmpl
└── main.go
2 directories, 2 files
asserts/templates.hello.tmpl
は以下に示すような単純なテンプレートである。
Hello, {{.}}!
また、 main.go
は自身の実行ファイル os.Executable()
をZIPファイルとして読み出し、一時ディレクトリに展開する。その後、展開されたリソース中のテンプレートを用いて Hello, World!
を出力する。
archive/zip
を使うときの注意点として、アーカイブ中のパスに ..
が無いことをチェックする必要がある。 ..
を許容すると任意のコマンドを実行される脆弱性につながる。
package main
import (
"archive/zip"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"text/template"
)
func main() {
exec, err := os.Executable()
if err != nil {
log.Fatalf("failed to get executable: %v", err)
}
r, err := zip.OpenReader(exec)
if err != nil {
log.Fatalf("failed to get zip reader: %v", err)
}
dir, err := ioutil.TempDir("", filepath.Base(exec))
if err != nil {
log.Fatalf("failed to open template directory: %v", err)
}
defer os.RemoveAll(dir)
for _, f := range r.File {
if err := extract(f, dir); err != nil {
log.Fatalf("failed to extract file: %v", err)
}
}
log.Printf("assets: %s", dir)
t := template.Must(template.ParseGlob(filepath.Join(dir, "templates", "*")))
if err := t.ExecuteTemplate(os.Stdout, "hello.tmpl", "World"); err != nil {
log.Fatalf("t.ExecuteTemplate() failed: %v", err)
}
}
func extract(f *zip.File, dir string) error {
if strings.Contains(f.Name, "..") {
// Zip Slip!
return fmt.Errorf("file path '%s' contains '..'", f.Name)
}
path := filepath.Join(dir, filepath.Clean(f.Name))
if f.Mode().IsDir() {
if err := os.MkdirAll(path, f.Mode()); err != nil {
return err
}
return nil
}
r, err := f.Open()
if err != nil {
return err
}
tf, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer tf.Close()
_, err = io.Copy(tf, r)
return err
}
実行ファイルの作成
まず通常の方法で実行ファイルを作成する。
$ go build -o hello
これにはリソースが埋め込まれていないため、必ず失敗する。
$ ./hello
2018/11/25 17:15:32 failed to get zip reader: zip: not a valid zip file
ZIPファイルの作成
次にリソースをまとめたZIPファイルを作成する。 zip
コマンドは実行時のカレントディレクトリを基準にするため assets
ディレクトリの中で実行する必要がある。
$ cd assets
$ zip -r ../assets.zip .
adding: templates/ (stored 0%)
adding: templates/hello.tmpl (stored 0%)
$ cd ..
正常に作られたリソースのZIPファイルにはパスに ..
や assets
が含まれない。
$ unzip -l assets.zip
Archive: assets.zip
Length Date Time Name
--------- ---------- ----- ----
0 11-25-2018 16:13 templates/
14 11-05-2018 21:31 templates/hello.tmpl
--------- -------
14 2 files
実行ファイル兼ZIPファイルの作成
次に、実行ファイルとZIPファイルを連結する。
$ cat hello assets.zip > hello-bundled
$ chmod +x hello-bundled
単純に連結しただけだとZIPファイル中のオフセット値が実行ファイルのサイズ分ずれてしまっているのでまだ失敗する。
$ ./hello-bundled
2018/11/25 17:16:37 failed to get zip reader: zip: not a valid zip file
$ unzip -l hello-bundled
Archive: hello-bundled
warning [hello-bundled]: 3309528 extra bytes at beginning or within zipfile
(attempting to process anyway)
Length Date Time Name
--------- ---------- ----- ----
0 11-25-2018 16:13 templates/
14 11-05-2018 21:31 templates/hello.tmpl
--------- -------
14 2 files
ここで zip -A
(--adjust-sfx
) するとオフセット値を調整することができる。
$ zip -A hello-bundled
Zip entry offsets appear off by 3309528 bytes - correcting...
調整が済めばself-extractingなZIPファイルになるため成功する。
$ ./hello-bundled
2018/11/25 17:17:12 assets: /var/folders/xt/6z9sk1dx1d734ltxxst16_h00000gn/T/hello-bundled725779200
Hello, World!
おわりに
ZIPを用いたリソースの埋め込み方法を紹介し、 archive/zip
を用いたリソース埋め込みの例を解説した。
実際にこの埋め込み方法を使うには go run
や go test
がそのままでは動かない。また、リソースを差し替えるにはビルドの手順をやり直す必要がある。
シンプルなインタフェースで go run
時や go test
時のフォールバックと環境変数を用いたリソースの差し替えに対応した assets というライブラリを作ったので試してみてほしい。
-
SFXスタブとは何か?ということについて書かれた仕様書等は見つけられなかった ↩