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

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

このコミットは、Go言語の archive/tar パッケージにおける USTAR 形式の長期ファイル名処理に関するバグ修正とテストの追加を扱っています。具体的には、特定の長いファイル名が USTAR 形式で正しく分割されないケースを修正し、それに伴うエラーを解消することを目的としています。

コミット

commit 4e998d71c9abf1a2d302307301688108ccfffc56
Author: Marco Hennings <marco.hennings@freiheit.com>
Date:   Fri Sep 6 16:49:38 2013 -0400

    archive/tar: fix a case where USTAR-split is not working correctly.
    
    For some long filenames the USTAR-split code does not work
    correctly. It is wrongly assumed that the path would not be too long,
    but it is.
    
    The user visible result was that a filename was split, but it still
    caused an error.
    
    The cause was a wrongly calculated nlen. In addition I noticed that
    at this place it is also seems necessary to check if the prefix will
    fit in the 155 chars available for the prefix.
    
    R=dsymonds, rsc
    CC=golang-dev
    https://golang.org/cl/13300046
---
 src/pkg/archive/tar/writer.go      |  7 +++++--
 src/pkg/archive/tar/writer_test.go | 36 ++++++++++++++++++++++++++++++++++++\n 2 files changed, 41 insertions(+), 2 deletions(-)

diff --git a/src/pkg/archive/tar/writer.go b/src/pkg/archive/tar/writer.go
index c0325194a2..549f1464c3 100644
--- a/src/pkg/archive/tar/writer.go
+++ b/src/pkg/archive/tar/writer.go
@@ -263,8 +263,11 @@ func (tw *Writer) splitUSTARLongName(name string) (prefix, suffix string, err er
 		length--
 	}
 	i := strings.LastIndex(name[:length], "/")
-	nlen := length - i - 1
-	if i <= 0 || nlen > fileNameSize || nlen == 0 {
+	// nlen contains the resulting length in the name field.
+	// plen contains the resulting length in the prefix field.
+	nlen := len(name) - i - 1
+	plen := i
+	if i <= 0 || nlen > fileNameSize || nlen == 0 || plen > fileNamePrefixSize {
 		err = errNameTooLong
 		return
 	}
diff --git a/src/pkg/archive/tar/writer_test.go b/src/pkg/archive/tar/writer_test.go
index cddcbbc254..30ebf977ac 100644
--- a/src/pkg/archive/tar/writer_test.go
+++ b/src/pkg/archive/tar/writer_test.go
@@ -355,3 +355,39 @@ func TestPAXHeader(t *testing.T) {
 		}
 	}
 }
+
+func TestUSTARLongName(t *testing.T) {
+	// Create an archive with a path that failed to split with USTAR extension in previous versions.
+	fileinfo, err := os.Stat("testdata/small.txt")
+	if err != nil {
+		t.Fatal(err)
+	}
+	hdr, err := FileInfoHeader(fileinfo, "")
+	hdr.Typeflag = TypeDir
+	if err != nil {
+		t.Fatalf("os.Stat:1 %v", err)
+	}
+	// Force a PAX long name to be written. The name was taken from a practical example
+	// that fails and replaced ever char through numbers to anonymize the sample.
+	longName := "/0000_0000000/00000-000000000/0000_0000000/00000-0000000000000/0000_0000000/00000-0000000-00000000/0000_0000000/00000000/0000_0000000/000/0000_0000000/00000000v00/0000_0000000/000000/0000_0000000/0000000/0000_0000000/00000y-00/0000/0000/00000000/0x000000/"
+	hdr.Name = longName
+
+	hdr.Size = 0
+	var buf bytes.Buffer
+	writer := NewWriter(&buf)
+	if err := writer.WriteHeader(hdr); err != nil {
+		t.Fatal(err)
+	}
+	if err := writer.Close(); err != nil {
+		t.Fatal(err)
+	}
+	// Test that we can get a long name back out of the archive.
+	reader := NewReader(&buf)
+	hdr, err = reader.Next()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if hdr.Name != longName {
+		t.Fatal("Couldn't recover long name")
+	}
+}

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

https://github.com/golang/go/commit/4e998d71c9abf1a2d302307301688108ccfffc56

元コミット内容

archive/tar: fix a case where USTAR-split is not working correctly.

For some long filenames the USTAR-split code does not work correctly. It is wrongly assumed that the path would not be too long, but it is.

The user visible result was that a filename was split, but it still caused an error.

The cause was a wrongly calculated nlen. In addition I noticed that at this place it is also seems necessary to check if the prefix will fit in the 155 chars available for the prefix.

R=dsymonds, rsc CC=golang-dev https://golang.org/cl/13300046

変更の背景

このコミットは、archive/tar パッケージが USTAR 形式で非常に長いファイル名を処理する際に発生していたバグを修正するために導入されました。既存の実装では、ファイル名が長すぎる場合に USTAR 形式の仕様に従って name フィールドと prefix フィールドに分割するロジックに誤りがありました。

具体的には、name フィールドの長さ (nlen) の計算が間違っていたため、分割されたファイル名が USTARname フィールドの最大長 (100文字) を超えてしまうことがありました。これにより、ファイル名が分割されたにもかかわらず、依然としてエラーが発生し、アーカイブの作成や読み取りが失敗するという問題がユーザーに影響を与えていました。

さらに、prefix フィールドが利用可能な155文字のスペースに収まるかどうかのチェックが不足していたことも問題の一因でした。このコミットは、これらの計算ミスとチェックの不足を修正し、USTAR 形式の長期ファイル名処理の堅牢性を向上させることを目的としています。

前提知識の解説

Tarアーカイブ形式

Tar (Tape ARchive) は、複数のファイルを一つのアーカイブファイルにまとめるためのファイル形式です。主にUnix系システムで利用され、ファイルのバックアップや配布によく用いられます。Tarアーカイブは、ファイルデータと、ファイル名、パーミッション、タイムスタンプなどのメタデータを含むヘッダーブロックで構成されます。

USTAR (POSIX 1003.1) 形式

初期のTar形式(V7 Tar)では、ファイル名の長さに100文字という制限がありました。この制限を克服するために、POSIX 1003.1標準で USTAR (Unix Standard TAR) 形式が導入されました。

USTAR 形式の主な特徴は、長いファイル名を扱うための prefix フィールドの導入です。

  • name フィールド: ファイル名の末尾100文字を格納します。
  • prefix フィールド: ファイル名の先頭部分を格納します。このフィールドの最大長は155文字です。

長いファイル名を USTAR 形式でアーカイブする場合、パスはディレクトリセパレータ(/)で分割され、name フィールドと prefix フィールドにそれぞれ格納されます。これにより、最大で256文字(prefix 155文字 + / 1文字 + name 100文字)までのファイル名を扱うことが可能になります。

重要なのは、パスがディレクトリセパレータで分割可能である必要がある点です。もし分割できない場合、USTAR 形式ではアーカイブできません。また、name および prefix フィールドは通常ヌル終端されますが、フィールド全体が名前の格納に使用される場合はヌル終端されないこともあります。一部の実装ではヌル終端を強制するため、実質的な長さが1バイト短くなることがあります(prefix は154文字、name は99文字)。

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

Go言語の標準ライブラリには、Tarアーカイブの作成と読み取りをサポートする archive/tar パッケージが含まれています。このパッケージは、Tarアーカイブの様々な形式(V7、USTAR、PAXなど)を扱うことができます。

Writer 構造体はTarアーカイブへの書き込みを担当し、WriteHeader メソッドを通じて各ファイルのヘッダー情報を書き込みます。この際、ファイル名が長い場合には splitUSTARLongName のような内部関数が呼び出され、USTAR 形式の仕様に従ってファイル名を分割する処理が行われます。

技術的詳細

このコミットの技術的な核心は、archive/tar パッケージ内の splitUSTARLongName 関数における nlen (name length) と plen (prefix length) の計算ロジックの修正にあります。

元のコードでは、nlen の計算が length - i - 1 となっていました。ここで length はファイル名の全長、i は最後のスラッシュのインデックスです。この計算は、name フィールドに格納されるべき部分の長さを誤って評価していました。

修正後のコードでは、nlenlen(name) - i - 1 となり、これはファイル名の全長から最後のスラッシュまでの部分(スラッシュ自体も含む)を引くことで、name フィールドに格納されるべきサフィックス部分の正確な長さを計算しています。

さらに重要な変更点として、prefix フィールドの長さ (plen) のチェックが追加されました。pleni、つまり最後のスラッシュのインデックスとして定義されています。これは、ファイル名の先頭から最後のスラッシュまでの部分が prefix フィールドに格納されるため、その長さが prefix フィールドの最大長 (fileNamePrefixSize、USTARでは155文字) を超えていないかを確認する必要があるためです。

修正前のコードでは、if i <= 0 || nlen > fileNameSize || nlen == 0 という条件のみでエラーチェックを行っていました。これは name フィールドの長さと、スラッシュの位置に関する基本的なチェックでしたが、prefix フィールドの長さに関する明示的なチェックが欠けていました。

修正後の条件 if i <= 0 || nlen > fileNameSize || nlen == 0 || plen > fileNamePrefixSize は、plen > fileNamePrefixSize という新しい条件を追加することで、prefix フィールドが USTAR の仕様で許容される最大長を超えていないかを厳密にチェックするようになりました。これにより、USTAR 形式の制約をより正確に遵守し、不正なファイル名分割によるエラーを防ぐことができます。

テストケース TestUSTARLongName は、この修正が正しく機能することを確認するために追加されました。このテストは、以前のバージョンで分割に失敗した特定の長いファイル名 (longName) を使用して、Writer が正しくアーカイブを作成し、Reader がそのアーカイブから元の長いファイル名を正確に復元できることを検証します。これは、バグが実際に修正され、リグレッションが発生しないことを保証するための重要なステップです。

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

src/pkg/archive/tar/writer.go

--- a/src/pkg/archive/tar/writer.go
+++ b/src/pkg/archive/tar/writer.go
@@ -263,8 +263,11 @@ func (tw *Writer) splitUSTARLongName(name string) (prefix, suffix string, err er
 		length--
 	}
 	i := strings.LastIndex(name[:length], "/")
-	nlen := length - i - 1
-	if i <= 0 || nlen > fileNameSize || nlen == 0 {
+	// nlen contains the resulting length in the name field.
+	// plen contains the resulting length in the prefix field.
+	nlen := len(name) - i - 1
+	plen := i
+	if i <= 0 || nlen > fileNameSize || nlen == 0 || plen > fileNamePrefixSize {
 		err = errNameTooLong
 		return
 	}

src/pkg/archive/tar/writer_test.go

--- a/src/pkg/archive/tar/writer_test.go
+++ b/src/pkg/archive/tar/writer_test.go
@@ -355,3 +355,39 @@ func TestPAXHeader(t *testing.T) {
 		}
 	}
 }
+
+func TestUSTARLongName(t *testing.T) {
+	// Create an archive with a path that failed to split with USTAR extension in previous versions.
+	fileinfo, err := os.Stat("testdata/small.txt")
+	if err != nil {
+		t.Fatal(err)
+	}
+	hdr, err := FileInfoHeader(fileinfo, "")
+	hdr.Typeflag = TypeDir
+	if err != nil {
+		t.Fatalf("os.Stat:1 %v", err)
+	}
+	// Force a PAX long name to be written. The name was taken from a practical example
+	// that fails and replaced ever char through numbers to anonymize the sample.
+	longName := "/0000_0000000/00000-000000000/0000_0000000/00000-0000000000000/0000_0000000/00000-0000000-00000000/0000_0000000/00000000/0000_0000000/000/0000_0000000/00000000v00/0000_0000000/000000/0000_0000000/0000000/0000_0000000/00000y-00/0000/0000/00000000/0x000000/"
+	hdr.Name = longName
+
+	hdr.Size = 0
+	var buf bytes.Buffer
+	writer := NewWriter(&buf)
+	if err := writer.WriteHeader(hdr); err != nil {
+		t.Fatal(err)
+	}
+	if err := writer.Close(); err != nil {
+		t.Fatal(err)
+	}
+	// Test that we can get a long name back out of the archive.
+	reader := NewReader(&buf)
+	hdr, err = reader.Next()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if hdr.Name != longName {
+		t.Fatal("Couldn't recover long name")
+	}
+}

コアとなるコードの解説

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

splitUSTARLongName 関数は、長いファイル名を USTAR 形式の prefixname フィールドに分割する役割を担っています。

  1. nlen の計算修正:

    • 変更前: nlen := length - i - 1
    • 変更後: nlen := len(name) - i - 1 この修正により、name フィールドに格納されるべきサフィックス部分の長さが正確に計算されるようになりました。len(name) は文字列 name の実際のバイト長を返します。
  2. plen の導入とチェック:

    • 新しく plen := i が導入されました。istrings.LastIndex によって見つけられた最後のスラッシュのインデックスであり、これは prefix フィールドに格納されるべき部分の長さに相当します。
    • エラー条件に || plen > fileNamePrefixSize が追加されました。これにより、計算された prefix の長さが USTAR 形式で許容される最大長 (155文字) を超えていないかどうかがチェックされるようになりました。もし超えている場合、errNameTooLong エラーが返され、不正なアーカイブの作成が防止されます。

これらの変更により、splitUSTARLongName 関数は USTAR 形式の長期ファイル名に関する仕様をより厳密に遵守し、以前発生していた誤った分割やエラーを解消します。

src/pkg/archive/tar/writer_test.go の変更点

TestUSTARLongName という新しいテスト関数が追加されました。

  1. テストデータの準備:

    • os.Stat("testdata/small.txt") を使用して、既存の小さなファイルから FileInfo を取得し、それを基に tar.Header を作成しています。これは、テスト用のダミーのファイル情報として利用されます。
    • hdr.Typeflag = TypeDir と設定されており、ディレクトリとして扱われることを示しています。
    • longName という非常に長いパス文字列が定義されています。この文字列は、以前のバージョンで USTAR 分割に失敗した実際の例を匿名化したものです。
  2. アーカイブの書き込み:

    • bytes.Buffer を使用してメモリ上にTarアーカイブを作成します。
    • NewWritertar.Writer を作成し、writer.WriteHeader(hdr)longName を持つヘッダーを書き込みます。
    • writer.Close() で書き込みを完了します。
  3. アーカイブの読み取りと検証:

    • NewReader で作成したアーカイブを読み取る tar.Reader を作成します。
    • reader.Next() で次のヘッダーを読み取ります。
    • if hdr.Name != longName で、読み取ったヘッダーのファイル名が元の longName と完全に一致するかどうかを検証します。一致しない場合はテストが失敗し、「Couldn't recover long name」というエラーが出力されます。

このテストは、splitUSTARLongName の修正が正しく機能し、非常に長いファイル名が USTAR 形式でアーカイブされ、その後正しく復元できることをエンドツーエンドで確認するものです。これにより、バグが修正されただけでなく、将来のリグレッションを防ぐための安全網が提供されます。

関連リンク

参考にした情報源リンク