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

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

このコミットは、Go言語の標準ライブラリであるarchive/tarパッケージに、拡張属性(Extended Attributes, xattrs)のサポートを追加するものです。具体的には、GNU tarやstarといったツールが生成するPAXヘッダ内のSCHILY.xattrフィールドを介して、ファイルの拡張属性を読み書きできるようになります。これにより、Goのarchive/tarパッケージがより広範なtarアーカイブ形式と互換性を持つようになり、特にLinuxなどのUnix系システムで利用されるファイルシステムの拡張属性を適切に扱えるようになります。

コミット

archive/tar: support extended attributes

This adds support for archives with the SCHILY.xattr field in the
pax header. This is what gnu tar and star generate.
Fixes #7154.

LGTM=dsymonds
R=golang-codereviews, gobot, dsymonds
CC=golang-codereviews
https://golang.org/cl/54570043

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

https://github.com/golang/go/commit/87d58f44a1e53a61c7bd4b11a7f7aa616f6e373d

元コミット内容

archive/tar: support extended attributes

This adds support for archives with the SCHILY.xattr field in the
pax header. This is what gnu tar and star generate.
Fixes #7154.

LGTM=dsymonds
R=golang-codereviews, gobot, dsymonds
CC=golang-codereviews
https://golang.org/cl/54570043

変更の背景

この変更の主な背景は、Goのarchive/tarパッケージが、GNU tarやstarなどの一般的なtar実装によって生成されたアーカイブに含まれる拡張属性を適切に処理できないという問題(Issue #7154)を解決することにありました。

従来のarchive/tarパッケージは、ファイル名、サイズ、パーミッションなどの基本的なメタデータは扱えましたが、ファイルシステムが提供する拡張属性(例えば、SELinuxのセキュリティコンテキストやユーザー定義のメタデータなど)は無視していました。これにより、拡張属性が重要な役割を果たすシステム(例えば、SELinuxが有効なLinuxシステム)で作成されたtarアーカイブをGoで処理する際に、情報が失われる可能性がありました。

このコミットは、PAX拡張ヘッダのSCHILY.xattrフィールドを認識し、これらの拡張属性をGoのtar.Header構造体に取り込むことで、互換性と機能性を向上させることを目的としています。

前提知識の解説

Tarアーカイブ形式

Tar(Tape Archive)は、複数のファイルを一つのアーカイブファイルにまとめるためのファイル形式です。元々は磁気テープへのバックアップのために開発されましたが、現在ではファイルやディレクトリの配布、転送、バックアップなど、幅広い用途で利用されています。

基本的なtarアーカイブは、各ファイルのエントリがヘッダブロックとデータブロックで構成されます。ヘッダブロックにはファイル名、サイズ、パーミッション、所有者などのメタデータが含まれます。

PAX拡張ヘッダ (POSIX.1-2001/2008)

従来のtar形式には、ファイル名の長さ制限(100文字)、ファイルサイズの制限(8GB)、タイムスタンプの精度不足など、いくつかの制約がありました。これらの制約を克服し、より柔軟なメタデータをサポートするために、POSIX.1-2001(およびその後のPOSIX.1-2008)でPAX(Portable Archive eXchange)拡張ヘッダが導入されました。

PAX拡張ヘッダは、通常のtarヘッダの後に続く特別なヘッダブロックで、キーと値のペアの形式で追加のメタデータを格納します。これにより、長いファイル名、大きなファイルサイズ、高精度なタイムスタンプ、そして今回焦点となる拡張属性など、従来のtarヘッダでは表現できなかった情報を格納できるようになります。

拡張属性 (Extended Attributes, xattrs)

拡張属性(Extended Attributes, xattrs)は、ファイルシステムレベルでファイルやディレクトリに関連付けられるメタデータのことです。通常のファイルデータや標準的なファイルシステムメタデータ(パーミッション、タイムスタンプなど)とは別に、追加の情報を格納するために使用されます。

xattrsの一般的な用途には以下のようなものがあります。

  • セキュリティコンテキスト: SELinuxやAppArmorなどの強制アクセス制御(MAC)システムが、ファイルにセキュリティラベルを付与するために使用します。
  • アクセス制御リスト (ACL): 標準的なUnixパーミッションよりも詳細なアクセス制御を設定するために使用されます。
  • ユーザー定義データ: アプリケーションがファイルに独自のメタデータを関連付けるために使用できます。例えば、写真ファイルに撮影場所やカメラモデルの情報を付与するなど。

xattrsはファイルシステムによってサポートが異なり、Linuxではext2/3/4、XFS、Btrfsなどがサポートしています。

SCHILY.xattrフィールド

SCHILY.xattrは、PAX拡張ヘッダ内で拡張属性を格納するために使用される特定のキーのプレフィックスです。これは、GNU tarやstarといった広く使われているtar実装が、ファイルシステムの拡張属性をアーカイブに含める際に採用しているデファクトスタンダードな形式です。

具体的には、SCHILY.xattr.attribute_name=attribute_valueという形式で、各拡張属性がPAXヘッダ内にキーと値のペアとして格納されます。例えば、SCHILY.xattr.security.selinux=unconfined_u:object_r:default_t:s0のように記述されます。

Go言語のarchive/tarパッケージ

Go言語の標準ライブラリには、archive/tarパッケージが含まれており、tarアーカイブの作成(書き込み)と展開(読み込み)の機能を提供します。このパッケージは、tar.Readertar.Writerという主要な型を通じて、ストリームベースでtarアーカイブを処理します。

  • tar.Reader: Next()メソッドでアーカイブ内の次のファイルまたはディレクトリのヘッダを読み込み、Read()メソッドでその内容を読み込みます。
  • tar.Writer: WriteHeader()メソッドでファイルのヘッダを書き込み、Write()メソッドでその内容を書き込みます。

tar.Header構造体は、アーカイブ内の各エントリのメタデータを表現します。

技術的詳細

このコミットは、archive/tarパッケージが拡張属性を透過的に扱えるように、以下の主要な変更を加えています。

  1. tar.Header構造体の拡張: src/pkg/archive/tar/common.goにおいて、Header構造体にXattrs map[string]stringフィールドが追加されました。これにより、tarアーカイブから読み込まれた拡張属性がこのマップに格納され、また、このマップに設定された拡張属性がtarアーカイブに書き込まれるようになります。キーは拡張属性名(例: user.keysecurity.selinux)、値はその属性値です。

  2. PAXヘッダからの拡張属性の読み込み: src/pkg/archive/tar/reader.gomergePAX関数が修正されました。この関数は、PAX拡張ヘッダから読み込まれたキーと値のペアをtar.Header構造体にマージする役割を担っています。 変更後、mergePAXは、キーがpaxXattrSCHILY.xattr.)プレフィックスで始まるかどうかをチェックします。もしそうであれば、プレフィックスを除いた部分を拡張属性のキーとして、対応する値をhdr.Xattrsマップに格納します。これにより、SCHILY.xattr形式で格納された拡張属性が正しくパースされ、Header構造体のXattrsフィールドに読み込まれるようになります。

  3. PAXヘッダへの拡張属性の書き込み: src/pkg/archive/tar/writer.gowriteHeader関数が修正されました。この関数は、tar.Header構造体の内容をtarヘッダ(必要に応じてPAXヘッダも含む)として書き込む役割を担っています。 変更後、writeHeaderは、hdr.Xattrsマップに拡張属性が設定されている場合、それらをPAXヘッダの形式(SCHILY.xattr.key=value)に変換し、paxHeadersマップに追加します。これにより、Header構造体に設定された拡張属性が、PAX拡張ヘッダとしてtarアーカイブに書き込まれるようになります。

  4. テストケースの追加と改善:

    • src/pkg/archive/tar/reader_test.goには、xattrs.tarという新しいテストデータファイルと、それに対応するuntarTestケースが追加されました。このテストケースは、拡張属性を含むtarアーカイブを読み込み、Header.Xattrsフィールドが期待通りにパースされることを検証します。特に、security.selinuxのような特殊な属性(ヌル文字を含む場合がある)も正しく扱えることを確認しています。
    • ヘッダの比較には、*hdr != *headerから!reflect.DeepEqual(*hdr, *header)への変更が行われました。これは、Header構造体にマップ型(Xattrs)が追加されたため、単純な構造体比較ではマップの内容を正しく比較できないためです。reflect.DeepEqualは、マップを含む複雑なデータ構造の深い比較を行うために必要です。
    • src/pkg/archive/tar/writer_test.goには、TestPaxXattrsという新しいテスト関数が追加されました。このテストは、Header.Xattrsに拡張属性を設定してtarアーカイブを書き込み、その後そのアーカイブを読み込み直して、拡張属性が正しくラウンドトリップ(書き込みと読み込み)できることを検証します。

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

src/pkg/archive/tar/common.go

--- a/src/pkg/archive/tar/common.go
+++ b/src/pkg/archive/tar/common.go
@@ -57,6 +57,7 @@ type Header struct {
 	Devminor   int64     // minor number of character or block device
 	AccessTime time.Time // access time
 	ChangeTime time.Time // status change time
+	Xattrs     map[string]string
 }
 
 // File name constants from the tar spec.
@@ -189,6 +190,7 @@ const (
 	paxSize     = "size"
 	paxUid      = "uid"
 	paxUname    = "uname"
+	paxXattr    = "SCHILY.xattr."
 	paxNone     = ""
 )

src/pkg/archive/tar/reader.go

--- a/src/pkg/archive/tar/reader.go
+++ b/src/pkg/archive/tar/reader.go
@@ -139,8 +139,14 @@ func mergePAX(hdr *Header, headers map[string]string) error {
 				return err
 			}
 			hdr.Size = int64(size)
+		default:
+			if strings.HasPrefix(k, paxXattr) {
+				if hdr.Xattrs == nil {
+					hdr.Xattrs = make(map[string]string)
+				}
+				hdr.Xattrs[k[len(paxXattr):]] = v
+			}
 		}
-
 	}
 	return nil
 }

src/pkg/archive/tar/writer.go

--- a/src/pkg/archive/tar/writer.go
+++ b/src/pkg/archive/tar/writer.go
@@ -236,6 +236,12 @@ func (tw *Writer) writeHeader(hdr *Header, allowPax bool) error {
 		return tw.err
 	}
 
+	if allowPax {
+		for k, v := range hdr.Xattrs {
+			paxHeaders[paxXattr+k] = v
+		}
+	}
+
 	if len(paxHeaders) > 0 {
 		if !allowPax {
 			return errInvalidHeader

src/pkg/archive/tar/reader_test.go

--- a/src/pkg/archive/tar/reader_test.go
+++ b/src/pkg/archive/tar/reader_test.go
@@ -180,7 +220,7 @@ testLoop:
 			f.Close()
 			continue testLoop
 		}
-		if *hdr != *header {
+		if !reflect.DeepEqual(*hdr, *header) {
 			t.Errorf("test %d, entry %d: Incorrect header:\nhave %+v\nwant %+v",
 				i, j, *hdr, *header)
 		}
@@ -253,7 +293,7 @@ func TestIncrementalRead(t *testing.T) {
 		}
 
 		// check the header
-		if *hdr != *headers[nread] {
+		if !reflect.DeepEqual(*hdr, *headers[nread]) {
 			t.Errorf("Incorrect header:\nhave %+v\nwant %+v",
 				*hdr, headers[nread])
 		}

コアとなるコードの解説

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

  • Header構造体へのXattrs map[string]stringの追加は、拡張属性をGoのプログラム内で表現するための基盤を構築します。map[string]string型は、拡張属性のキーと値を文字列として格納するのに適しています。
  • paxXattr = "SCHILY.xattr."定数の追加は、PAXヘッダ内で拡張属性を識別するためのプレフィックスを定義します。これにより、コード全体で一貫してこのプレフィックスを使用できるようになります。

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

mergePAX関数は、PAX拡張ヘッダから読み取られたキーと値のペアを処理します。 変更されたdefaultケースでは、読み取られたキーkpaxXattr"SCHILY.xattr.")で始まるかどうかをstrings.HasPrefixで確認します。 もしプレフィックスが一致すれば、それは拡張属性であると判断されます。

  • hdr.Xattrs == nilのチェックは、Xattrsマップがまだ初期化されていない場合にmake(map[string]string)で初期化を行います。これにより、nilマップへの書き込みを防ぎます。
  • hdr.Xattrs[k[len(paxXattr):]] = vの行が、実際の拡張属性の格納を行います。k[len(paxXattr):]は、キーから"SCHILY.xattr."プレフィックスを取り除き、純粋な拡張属性名(例: "user.key")を取得します。その属性名と対応する値vXattrsマップに格納されます。

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

writeHeader関数は、tar.Header構造体の内容をtarアーカイブに書き込む際に、PAXヘッダを生成する必要がある場合に呼び出されます。

  • if allowPaxブロック内で、hdr.Xattrsマップがイテレートされます。
  • paxHeaders[paxXattr+k] = vの行は、Header構造体のXattrsマップに格納されている各拡張属性を、PAXヘッダの形式(SCHILY.xattr.key=value)に変換してpaxHeadersマップに追加します。このpaxHeadersマップは、最終的にPAXヘッダとしてアーカイブに書き込まれます。

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

テストコードにおける!reflect.DeepEqual(*hdr, *header)への変更は非常に重要です。 Goにおいて、構造体の比較演算子==は、構造体のすべてのフィールドが比較可能であり、かつそれらが等しい場合にtrueを返します。しかし、マップ型は比較可能ではないため、構造体内にマップが含まれる場合、==演算子では正しく比較できません。 reflect.DeepEqual関数は、2つの値が「深く」等しいかどうかを再帰的にチェックします。これは、ポインタが指す値、配列、スライス、マップ、構造体など、複雑なデータ構造の内容を比較する際に特に有用です。 Header構造体にXattrsというマップフィールドが追加されたため、この変更はテストが正しく機能するために不可欠でした。

これらの変更により、Goのarchive/tarパッケージは、拡張属性を含むtarアーカイブを完全にサポートし、より堅牢で互換性の高いアーカイブ処理を提供できるようになりました。

関連リンク

参考にした情報源リンク