Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 15221] ファイルの概要

このコミットは、Go言語の標準ライブラリ archive/tar パッケージにおける、ディレクトリのヘッダー情報生成時の挙動に関する修正です。具体的には、ディレクトリのエントリ名に末尾のスラッシュ (/) を付加するよう変更されました。これにより、GNU tar 1.26 との互換性が向上します。

コミット

commit 96082a6953583b88b73cf45933325648b66c1654
Author: Christian Himpel <chressie@googlemail.com>
Date:   Wed Feb 13 19:23:28 2013 +1100

    archive/tar: append a slash when deriving header info from a directory
    
    This behavior is identical to GNU tar 1.26.
    
    R=dsymonds, dave
    CC=golang-dev
    https://golang.org/cl/7307101

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/96082a6953583b88b73cf45933325648b66c1654

元コミット内容

archive/tar: append a slash when deriving header info from a directory

このコミットは、ディレクトリからヘッダー情報を生成する際に、名前にスラッシュを付加する変更を導入します。この挙動はGNU tar 1.26と同一です。

変更の背景

TAR (Tape Archive) フォーマットは、複数のファイルを一つのアーカイブにまとめるための標準的な形式です。このフォーマットは、ファイルだけでなくディレクトリの構造もアーカイブ内に保持します。しかし、ディレクトリのエントリ名をどのように表現するかについては、様々な実装間で微妙な違いが存在することがあります。

特に、ディレクトリのエントリ名に末尾のスラッシュ (/) を付加するかどうかは、TARアーカイブを扱うツール間で互換性の問題を引き起こす可能性があります。一部のTAR実装(特にGNU tar)では、ディレクトリを示すエントリ名には慣習的に末尾にスラッシュを付加します。これは、そのエントリがファイルではなくディレクトリであることを明示的に示すためのものです。

Go言語の archive/tar パッケージは、TARアーカイブの読み書きをサポートする標準ライブラリです。このパッケージが生成するTARアーカイブが、他の広く使われているTARツール(例えばGNU tar)と完全に互換性を持つことは非常に重要です。このコミット以前は、Goの archive/tar パッケージがディレクトリのヘッダーを生成する際に、末尾のスラッシュを付加していませんでした。この差異が、GNU tarなどのツールでアーカイブを抽出する際に予期せぬ挙動やエラーを引き起こす可能性がありました。

この変更の背景には、Goの archive/tar パッケージが生成するアーカイブの互換性を向上させ、より堅牢で標準的な挙動に合わせるという目的があります。具体的には、GNU tar 1.26の挙動に合わせることで、Goで作成されたTARアーカイブが、広く利用されている他のシステムやツールで問題なく扱えるようにすることが意図されています。

前提知識の解説

TAR (Tape Archive) フォーマット

TARは、複数のファイルやディレクトリを単一のアーカイブファイルにまとめるためのファイルフォーマットです。元々は磁気テープにデータを保存するために開発されましたが、現在ではファイルシステム上のアーカイブとしても広く利用されています。TARファイルは、個々のファイルやディレクトリのメタデータ(パーミッション、所有者、タイムスタンプなど)と内容を順番に格納します。

TARヘッダー

TARアーカイブ内の各ファイルまたはディレクトリは、その内容の前に「ヘッダー」と呼ばれるメタデータブロックを持ちます。このヘッダーには、ファイル名、サイズ、パーミッション、最終更新日時、ファイルタイプ(通常ファイル、ディレクトリ、シンボリックリンクなど)といった情報が含まれます。

TypeflagName フィールド

TARヘッダーには、ファイルの種類を示す Typeflag フィールドと、ファイルまたはディレクトリの名前を示す Name フィールドがあります。

  • Typeflag: TypeDir はディレクトリを示します。
  • Name: ファイルまたはディレクトリのパス名が格納されます。

ディレクトリ名の慣習と互換性

TARフォーマットの仕様自体は、ディレクトリ名に末尾のスラッシュを必須とはしていません。しかし、多くのTAR実装、特にGNU tarでは、ディレクトリのエントリ名には末尾にスラッシュを付加することが一般的です。これは、ファイル名とディレクトリ名を区別しやすくするため、また一部のツールがこのスラッシュの有無によって挙動を変えるためです。

例えば、my_directory というエントリがあった場合、これがファイルなのかディレクトリなのかは Typeflag を見ればわかりますが、my_directory/ となっていれば、名前を見ただけでディレクトリであることが直感的に理解できます。この慣習に従うことで、異なるTARツール間での互換性が高まります。

os.FileInfoos.Stat/os.Lstat

Go言語の os パッケージは、ファイルシステム操作のための機能を提供します。

  • os.FileInfo: ファイルに関する情報(名前、サイズ、モード、最終更新日時など)を抽象化したインターフェースです。
  • os.Stat(path string): 指定されたパスのファイル情報を返します。パスがシンボリックリンクの場合、そのリンクが指す先のファイル情報を返します(シンボリックリンクを解決します)。
  • os.Lstat(path string): 指定されたパスのファイル情報を返します。パスがシンボリックリンクの場合、シンボリックリンク自体の情報を返します(シンボリックリンクを解決しません)。

このコミットでは、テストコードで os.Lstatos.Stat に変更されていますが、これは testdata/small.txt が通常のファイルであるため、この特定のケースでは挙動に大きな違いはありません。しかし、一般的には os.Stat がシンボリックリンクを解決するのに対し、os.Lstat は解決しないという違いがあります。

技術的詳細

このコミットの主要な変更は、src/pkg/archive/tar/common.go ファイル内の FileInfoHeader 関数にあります。この関数は、os.FileInfo オブジェクトとシンボリックリンクのターゲットパス(もしあれば)を受け取り、それに基づいて tar.Header 構造体を生成します。

変更前は、fi.IsDir() が真(つまり、os.FileInfo がディレクトリを表している)の場合、h.TypeflagTypeDir に設定し、h.Modec_ISDIR フラグを追加するだけでした。変更後は、これに加えて h.Name += "/" という行が追加されました。これにより、ディレクトリのヘッダーが生成される際に、その名前に末尾のスラッシュが自動的に付加されるようになります。

この変更は、TARアーカイブの標準的な慣習に合わせるためのものであり、特にGNU tarとの互換性を確保することを目的としています。GNU tarは、ディレクトリのエントリ名に末尾のスラッシュがあることを期待する場合があります。このスラッシュがないと、GNU tarがアーカイブを抽出する際に、ディレクトリとして正しく認識されなかったり、警告を発したり、あるいは予期せぬファイルとして扱われたりする可能性があります。

テストファイル src/pkg/archive/tar/tar_test.go には、この新しい挙動を検証するための TestFileInfoHeaderDir という新しいテスト関数が追加されました。このテストは、testdata ディレクトリの os.FileInfo を取得し、FileInfoHeader 関数に渡して生成されたヘッダーの Name フィールドが testdata/ となっていることを確認します。これにより、ディレクトリ名にスラッシュが正しく付加されていることが保証されます。

コアとなるコードの変更箇所

src/pkg/archive/tar/common.go

--- a/src/pkg/archive/tar/common.go
+++ b/src/pkg/archive/tar/common.go
@@ -79,6 +79,7 @@ const (
 
 // FileInfoHeader creates a partially-populated Header from fi.
 // If fi describes a symlink, FileInfoHeader records link as the link target.
+// If fi describes a directory, a slash is appended to the name.
 func FileInfoHeader(fi os.FileInfo, link string) (*Header, error) {
  	if fi == nil {
  		return nil, errors.New("tar: FileInfo is nil")
@@ -96,6 +97,7 @@ func FileInfoHeader(fi os.FileInfo, link string) (*Header, error) {
  	case fi.IsDir():
  	\th.Typeflag = TypeDir
  	\th.Mode |= c_ISDIR
+\t\th.Name += "/"
  	case fi.Mode()&os.ModeSymlink != 0:\
  	\th.Typeflag = TypeSymlink
  	\th.Mode |= c_ISLNK

src/pkg/archive/tar/tar_test.go

--- a/src/pkg/archive/tar/tar_test.go
+++ b/src/pkg/archive/tar/tar_test.go
@@ -14,13 +14,13 @@ import (
 )
 
 func TestFileInfoHeader(t *testing.T) {
-\tfi, err := os.Lstat("testdata/small.txt")
+\tfi, err := os.Stat("testdata/small.txt")
  	if err != nil {
  	\tt.Fatal(err)
  	}
  	h, err := FileInfoHeader(fi, "")
  	if err != nil {
-\t\tt.Fatalf("on small.txt: %v", err)
+\t\tt.Fatalf("FileInfoHeader: %v", err)
  	}
  	if g, e := h.Name, "small.txt"; g != e {
  	\tt.Errorf("Name = %q; want %q", g, e)
@@ -36,6 +36,29 @@ func TestFileInfoHeader(t *testing.T) {
  	}
  }
 
+func TestFileInfoHeaderDir(t *testing.T) {
+\tfi, err := os.Stat("testdata")
+\tif err != nil {
+\t\tt.Fatal(err)
+\t}\
+\th, err := FileInfoHeader(fi, "")
+\tif err != nil {
+\t\tt.Fatalf("FileInfoHeader: %v", err)
+\t}\
+\tif g, e := h.Name, "testdata/"; g != e {
+\t\tt.Errorf("Name = %q; want %q", g, e)
+\t}\
+\tif g, e := h.Mode, int64(fi.Mode().Perm())|c_ISDIR; g != e {
+\t\tt.Errorf("Mode = %#o; want %#o", g, e)\
+\t}\
+\tif g, e := h.Size, int64(0); g != e {
+\t\tt.Errorf("Size = %v; want %v", g, e)\
+\t}\
+\tif g, e := h.ModTime, fi.ModTime(); !g.Equal(e) {
+\t\tt.Errorf("ModTime = %v; want %v", g, e)\
+\t}\
+}\
+\
  func TestFileInfoHeaderSymlink(t *testing.T) {
  \th, err := FileInfoHeader(symlink{}, "some-target")
  \tif err != nil {

コアとなるコードの解説

src/pkg/archive/tar/common.go の変更

FileInfoHeader 関数は、os.FileInfo から tar.Header を構築する際の中心的なロジックを含んでいます。この関数内で、switch ステートメントによってファイルの種類が判別されます。

func FileInfoHeader(fi os.FileInfo, link string) (*Header, error) {
    // ... (省略) ...
    switch {
    case fi.IsDir(): // fi がディレクトリを表す場合
        h.Typeflag = TypeDir // ヘッダーのタイプをディレクトリに設定
        h.Mode |= c_ISDIR    // モードにディレクトリフラグを追加
        h.Name += "/"        // ここでディレクトリ名に末尾のスラッシュを追加
    // ... (他のケース、シンボリックリンクなど) ...
    }
    // ... (省略) ...
}

この h.Name += "/" の一行が、ディレクトリのヘッダー名にスラッシュを付加するという今回の変更の核心です。これにより、Goの archive/tar パッケージが生成するTARアーカイブ内のディレクトリ名が、GNU tarなどの他のツールが期待する形式に合致するようになります。

src/pkg/archive/tar/tar_test.go の変更

テストファイルでは、主に以下の2つの変更が行われました。

  1. TestFileInfoHeader 関数の os.Lstat から os.Stat への変更:

    -	fi, err := os.Lstat("testdata/small.txt")
    +	fi, err := os.Stat("testdata/small.txt")
    

    これは、small.txt が通常のファイルであるため、この特定のテストケースでは機能的な違いはほとんどありません。しかし、一般的に os.Stat はシンボリックリンクを解決してその実体の情報を取得するのに対し、os.Lstat はシンボリックリンク自体の情報を取得します。この変更は、テストの意図をより明確にするか、あるいは将来的なシンボリックリンクのテストケースへの対応を見越したものかもしれません。

  2. TestFileInfoHeaderDir 関数の追加:

    func TestFileInfoHeaderDir(t *testing.T) {
        fi, err := os.Stat("testdata") // "testdata" ディレクトリの情報を取得
        if err != nil {
            t.Fatal(err)
        }
        h, err := FileInfoHeader(fi, "") // ディレクトリ情報からヘッダーを生成
        if err != nil {
            t.Fatalf("FileInfoHeader: %v", err)
        }
        if g, e := h.Name, "testdata/"; g != e { // 生成された名前が "testdata/" であることを確認
            t.Errorf("Name = %q; want %q", g, e)
        }
        // ... (他のフィールドの検証) ...
    }
    

    この新しいテスト関数は、ディレクトリの os.FileInfoFileInfoHeader 関数に渡し、その結果として得られる HeaderName フィールドが期待通りに末尾にスラッシュが付加されているかを確認します。これは、common.go で行われた変更が正しく機能していることを保証するための重要な単体テストです。

これらの変更により、Goの archive/tar パッケージは、より標準的で互換性の高いTARアーカイブを生成できるようになり、特にディレクトリの表現に関して他のTARツールとの相互運用性が向上しました。

関連リンク

参考にした情報源リンク