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

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

このコミットは、Go言語の標準ライブラリ archive/tar パッケージにおける機能拡張とバグ修正に関するものです。具体的には、tar.Header 構造体から os.FileInfo インターフェースを実装したオブジェクトを取得するための FileInfo() メソッドが追加され、FileInfoHeader 関数が FIFO (名前付きパイプ)、setuid、setgid、およびスティッキービットといった特殊なファイルモードを適切に処理できるよう改善されました。これにより、tarアーカイブのメタデータとGoのファイルシステム抽象化との間の相互運用性が向上し、より多様なファイルタイプやパーミッション属性を正確に表現できるようになります。

コミット

commit 44d38ae3c0f44449933a5e56ed4282fabb2ffb09
Author: Robin Eklind <r.eklind.87@gmail.com>
Date:   Thu Feb 14 17:32:48 2013 +1100

    archive/tar: add Header.FileInfo method. Add more cases to FileInfoHeader.
    
    FileInfoHeader can now handle fifo, setuid, setgid and sticky bits.
    
    Fixes #4695.
    
    R=golang-dev, donovanhide, r.eklind.87, minux.ma, adg
    CC=golang-dev
    https://golang.org/cl/7305072

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

https://github.com/golang/go/commit/44d38ae3c0f44449933a5e56ed4282fabb2ffb09

元コミット内容

archive/tar: Header.FileInfo メソッドを追加。FileInfoHeader により多くのケースを追加。

FileInfoHeader は、FIFO、setuid、setgid、およびスティッキービットを処理できるようになりました。

Fixes #4695.

変更の背景

この変更の背景には、Go言語の archive/tar パッケージが、tarアーカイブ内のファイルメタデータをより正確かつ包括的に表現する必要があったという点があります。

  1. os.FileInfo との相互運用性: Goの標準ライブラリには、ファイルシステムオブジェクトの情報を抽象化するための os.FileInfo インターフェースが存在します。archive/tar パッケージが tar ヘッダー情報を os.FileInfo として直接提供できるようになることで、Goの他のファイルシステム関連APIとの連携が容易になり、開発者は tar アーカイブ内のファイルを通常のファイルシステムオブジェクトと同じように扱えるようになります。これにより、コードの再利用性が高まり、tarアーカイブの操作がより直感的になります。

  2. 特殊なファイルモードのサポート不足: 以前の FileInfoHeader 関数は、通常のファイル、ディレクトリ、シンボリックリンク、デバイスファイルなどの基本的なファイルタイプは処理できましたが、FIFO (名前付きパイプ)、setuid、setgid、スティッキービットといった特殊なパーミッションビットを適切に変換できませんでした。これらのビットはUnix系システムにおいて重要なセキュリティおよび動作特性を定義するため、tarアーカイブがこれらの属性を保持し、Goプログラムがそれらを正確に解釈できることは、アーカイブの完全性と互換性を保証する上で不可欠でした。特に、Fixes #4695 という記述から、この機能不足が具体的なバグとして報告されていたことが伺えます。

これらの課題を解決し、archive/tar パッケージの堅牢性と実用性を向上させることが、このコミットの主な動機となっています。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. tarアーカイブ形式:

    • 概要: tar (tape archive) は、複数のファイルを1つのアーカイブファイルにまとめるためのファイル形式です。主にUnix系システムでファイルのバックアップや配布に利用されます。
    • ヘッダー (Header): tarアーカイブ内の各ファイルやディレクトリは、その内容の前に「ヘッダー」と呼ばれるメタデータブロックを持ちます。このヘッダーには、ファイル名、サイズ、パーミッション、所有者、更新日時などの情報が含まれます。
    • ファイルモード (Mode): tarヘッダー内の「モード」フィールドは、Unixのファイルパーミッション(読み取り、書き込み、実行権限)だけでなく、ファイルの種類(通常ファイル、ディレクトリ、シンボリックリンク、デバイスファイルなど)や特殊なパーミッションビット(setuid, setgid, sticky bit)もエンコードします。これは通常、8進数で表現されます(例: 0755)。
  2. Go言語の os パッケージと os.FileInfo:

    • os.FileInfo インターフェース: Go言語の os パッケージは、オペレーティングシステムとのインタラクションを提供します。os.FileInfo は、ファイルやディレクトリに関する情報を抽象化するためのインターフェースです。このインターフェースは、Name(), Size(), Mode(), ModTime(), IsDir(), Sys() などのメソッドを持ち、ファイルの種類、サイズ、パーミッション、最終更新日時などを取得できます。
    • os.FileMode: os.FileMode は、ファイルのパーミッションとモードビットを表す型です。Unixのファイルモードに対応しており、os.ModeDir (ディレクトリ), os.ModeSymlink (シンボリックリンク), os.ModeDevice (デバイスファイル), os.ModeNamedPipe (FIFO), os.ModeSetuid (setuidビット), os.ModeSetgid (setgidビット), os.ModeSticky (スティッキービット) などの定数が定義されています。
  3. Unixの特殊なパーミッションビット:

    • setuid (Set User ID) ビット (04000): 実行可能ファイルにこのビットが設定されていると、そのファイルを実行したユーザーのIDではなく、ファイルの所有者のIDでプロセスが実行されます。これにより、一般ユーザーが通常アクセスできないリソースに、一時的にファイルの所有者の権限でアクセスできるようになります(例: passwd コマンド)。
    • setgid (Set Group ID) ビット (02000): 実行可能ファイルに設定されている場合、setuidと同様に、ファイルのグループIDでプロセスが実行されます。ディレクトリに設定されている場合、そのディレクトリ内に作成される新しいファイルやサブディレクトリは、親ディレクトリのグループIDを継承します。
    • スティッキービット (Sticky Bit) (01000): ディレクトリにこのビットが設定されていると、そのディレクトリ内のファイルは、ファイルの所有者、ディレクトリの所有者、またはrootユーザーのみが削除または名前変更できます。他のユーザーは、たとえ書き込み権限があっても、自分が所有していないファイルを削除できません。これは、/tmp のような共有ディレクトリでよく使用され、ユーザーが互いのファイルを誤って削除するのを防ぎます。
    • FIFO (First-In, First-Out) / 名前付きパイプ: ファイルシステム上に存在する特殊なファイルで、プロセス間通信 (IPC) の一種として機能します。一方のプロセスがFIFOに書き込んだデータは、もう一方のプロセスがFIFOから読み取ることができます。通常のファイルとは異なり、データは永続的に保存されず、読み取られると消滅します。

これらの概念を理解することで、コミットが archive/tar パッケージの機能とGoのファイルシステム抽象化をどのように統合し、Unixのファイルシステム属性をより正確に扱うように改善したかが明確になります。

技術的詳細

このコミットは、archive/tar パッケージにおける Header 構造体と os.FileInfo インターフェース間の変換ロジックを強化しています。

Header.FileInfo() メソッドの追加

  • 目的: tar.Header 構造体から直接 os.FileInfo インターフェースを実装するオブジェクトを取得できるようにします。これにより、tarアーカイブ内のファイルメタデータをGoの標準的なファイル情報インターフェースを通じて統一的に扱えるようになります。
  • 実装:
    • headerFileInfo という内部構造体が定義され、これが os.FileInfo インターフェースを実装します。
    • headerFileInfo*Header を内部に持ち、os.FileInfo の各メソッド(Name(), Size(), IsDir(), ModTime(), Mode(), Sys())は、内部の Header の対応するフィールドやロジックに基づいて値を返します。
    • 特に Mode() メソッドは、tar.HeaderMode フィールドと Typeflag フィールドを組み合わせて、正確な os.FileMode を構築します。これには、通常のパーミッションビット (Perm()) に加えて、setuid, setgid, sticky ビット、そしてディレクトリ、FIFO、シンボリックリンク、ブロックデバイス、キャラクターデバイス、ソケットといったファイルタイプを示すビットが含まれます。

FileInfoHeader 関数の拡張

  • 目的: os.FileInfo オブジェクトから tar.Header 構造体を生成する FileInfoHeader 関数が、より多くの os.FileMode の種類を正確に tar.Header に変換できるように拡張されました。
  • 変更点:
    • 以前は os.ModeNamedPipe (FIFO), os.ModeSetuid, os.ModeSetgid, os.ModeSticky といった特殊なモードビットが適切に処理されていませんでした。
    • このコミットにより、FileInfoHeaderos.FileMode のこれらのビットを検出し、対応する tar.HeaderMode フィールドに c_ISFIFO, c_ISUID, c_ISGID, c_ISVTX といった tar 固有のモード定数を適切に設定するようになりました。
    • これにより、Goの os.FileInfo から生成された tar.Header が、元のファイルのすべての重要な属性(特にパーミッションとファイルタイプ)を正確に保持できるようになります。

c_IS* 定数の明確化

  • src/pkg/archive/tar/common.go 内で定義されている c_IS* 定数(例: c_ISDIR, c_ISFIFO)に、それぞれの意味(例: // Directory, // FIFO)がコメントとして追加され、コードの可読性が向上しました。また、setuid, setgid, sticky ビットに対応する c_ISUID, c_ISGID, c_ISVTX 定数が追加されました。

テストの追加

  • src/pkg/archive/tar/tar_test.goTestHeaderRoundTrip という新しいテスト関数が追加されました。
  • このテストは、様々なファイルタイプとパーミッション(通常のファイル、ハードリンク、シンボリックリンク、キャラクターデバイス、ブロックデバイス、ディレクトリ、FIFO、setuid、setgid、スティッキービットを持つファイル)を持つ tar.Header オブジェクトを作成し、それらを FileInfo() メソッドで os.FileInfo に変換し、さらに FileInfoHeader() 関数で元の tar.Header に戻すというラウンドトリップテストを行います。
  • これにより、Header.FileInfo()FileInfoHeader() の両関数が、これらの特殊なファイルモードを正確に処理し、情報の損失なく変換できることが検証されます。

これらの変更により、archive/tar パッケージは、より広範なファイルシステム属性をサポートし、Goのファイルシステム抽象化との連携を強化することで、より堅牢で互換性の高いtarアーカイブ処理を提供できるようになりました。

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

このコミットにおける主要なコード変更は、src/pkg/archive/tar/common.gosrc/pkg/archive/tar/tar_test.go の2つのファイルに集中しています。

src/pkg/archive/tar/common.go

  1. Header.FileInfo() メソッドの追加:

    // FileInfo returns an os.FileInfo for the Header.
    func (h *Header) FileInfo() os.FileInfo {
    	return headerFileInfo{h}
    }
    
    // headerFileInfo implements os.FileInfo.
    type headerFileInfo struct {
    	h *Header
    }
    
    func (fi headerFileInfo) Size() int64        { return fi.h.Size }
    func (fi headerFileInfo) IsDir() bool        { return fi.Mode().IsDir() }
    func (fi headerFileInfo) ModTime() time.Time { return fi.h.ModTime }
    func (fi headerFileInfo) Sys() interface{}   { return fi.h }
    
    // Name returns the base name of the file.
    func (fi headerFileInfo) Name() string {
    	if fi.IsDir() {
    		return path.Clean(fi.h.Name)
    	}
    	return fi.h.Name
    }
    
    // Mode returns the permission and mode bits for the headerFileInfo.
    func (fi headerFileInfo) Mode() (mode os.FileMode) {
    	// Set file permission bits.
    	mode = os.FileMode(fi.h.Mode).Perm()
    
    	// Set setuid, setgid and sticky bits.
    	if fi.h.Mode&c_ISUID != 0 {
    		// setuid
    		mode |= os.ModeSetuid
    	}
    	if fi.h.Mode&c_ISGID != 0 {
    		// setgid
    		mode |= os.ModeSetgid
    	}
    	if fi.h.Mode&c_ISVTX != 0 {
    		// sticky
    		mode |= os.ModeSticky
    	}
    
    	// Set file mode bits.
    	// clear perm, setuid, setgid and sticky bits.
    	m := os.FileMode(fi.h.Mode) &^ 07777
    	if m == c_ISDIR {
    		// directory
    		mode |= os.ModeDir
    	}
    	if m == c_ISFIFO {
    		// named pipe (FIFO)
    		mode |= os.ModeNamedPipe
    	}
    	if m == c_ISLNK {
    		// symbolic link
    		mode |= os.ModeSymlink
    	}
    	if m == c_ISBLK {
    		// device file
    		mode |= os.ModeDevice
    	}
    	if m == c_ISCHR {
    		// Unix character device
    		mode |= os.ModeDevice
    		mode |= os.ModeCharDevice
    	}
    	if m == c_ISSOCK {
    		// Unix domain socket
    		mode |= os.ModeSocket
    	}
    
    	switch fi.h.Typeflag {
    	case TypeLink, TypeSymlink:
    		// hard link, symbolic link
    		mode |= os.ModeSymlink
    	case TypeChar:
    		// character device node
    		mode |= os.ModeDevice
    		mode |= os.ModeCharDevice
    	case TypeBlock:
    		// block device node
    		mode |= os.ModeDevice
    	case TypeDir:
    		// directory
    		mode |= os.ModeDir
    	case TypeFifo:
    		// fifo node
    		mode |= os.ModeNamedPipe
    	}
    
    	return mode
    }
    
  2. c_IS* 定数の追加とコメントの明確化:

    const (
    	c_ISUID  = 04000   // Set uid
    	c_ISGID  = 02000   // Set gid
    	c_ISVTX  = 01000   // Save text (sticky bit)
    	c_ISDIR  = 040000  // Directory
    	c_ISFIFO = 010000  // FIFO
    	c_ISREG  = 0100000 // Regular file
    	c_ISLNK  = 0120000 // Symbolic link
    	c_ISBLK  = 060000  // Block special file
    	c_ISCHR  = 020000  // Character special file
    	c_ISSOCK = 0140000 // Socket
    )
    
  3. FileInfoHeader 関数の拡張:

    func FileInfoHeader(fi os.FileInfo, link string) (*Header, error) {
    	if fi == nil {
    		return nil, errors.New("tar: FileInfo is nil")
    	}
    	fm := fi.Mode() // fi.Mode() を一度変数に格納
    	h := &Header{
    		Name:    fi.Name(),
    		ModTime: fi.ModTime(),
    		Mode:    int64(fm.Perm()), // fm を使用
    	}
    	switch {
    	case fm.IsRegular(): // fm を使用
    		h.Mode |= c_ISREG
    		h.Typeflag = TypeReg
    		h.Size = fi.Size()
    	case fm.IsDir(): // fm を使用
    		h.Typeflag = TypeDir
    		h.Mode |= c_ISDIR
    		h.Name += "/"
    	case fm&os.ModeSymlink != 0: // fm を使用
    		h.Typeflag = TypeSymlink
    		h.Mode |= c_ISLNK
    		h.Linkname = link
    	case fm&os.ModeDevice != 0: // fm を使用
    		if fm&os.ModeCharDevice != 0 { // fm を使用
    			h.Mode |= c_ISCHR
    			h.Typeflag = TypeChar
    		} else {
    			h.Mode |= c_ISBLK
    			h.Typeflag = TypeBlock
    		}
    	case fm&os.ModeNamedPipe != 0: // 新規追加
    		h.Typeflag = TypeFifo
    		h.Mode |= c_ISFIFO
    	case fm&os.ModeSocket != 0: // fm を使用
    		h.Mode |= c_ISSOCK
    	default:
    		return nil, fmt.Errorf("archive/tar: unknown file mode %v", fm) // fm を使用
    	}
    	// setuid, setgid, sticky ビットの処理を追加
    	if fm&os.ModeSetuid != 0 {
    		h.Mode |= c_ISUID
    	}
    	if fm&os.ModeSetgid != 0 {
    		h.Mode |= c_ISGID
    	}
    	if fm&os.ModeSticky != 0 {
    		h.Mode |= c_ISVTX
    	}
    	if sysStat != nil {
    		return h, sysStat(fi, h)
    	}
    	return h, nil
    }
    

src/pkg/archive/tar/tar_test.go

  1. TestHeaderRoundTrip テスト関数の追加: このテストは非常に長く、様々なファイルタイプとモード(通常のファイル、ハードリンク、シンボリックリンク、キャラクターデバイス、ブロックデバイス、ディレクトリ、FIFO、setuid、setgid、スティッキービット)を網羅した headerRoundTripTest のスライス golden を定義しています。各テストケースでは、Header から FileInfo() を呼び出して os.FileInfo を取得し、さらに FileInfoHeader() を呼び出して Header に戻し、元の値と比較することで、変換の正確性を検証しています。

    type headerRoundTripTest struct {
    	h  *Header
    	fm os.FileMode
    }
    
    func TestHeaderRoundTrip(t *testing.T) {
    	golden := []headerRoundTripTest{
    		// regular file.
    		{
    			h: &Header{
    				Name:     "test.txt",
    				Mode:     0644 | c_ISREG,
    				Size:     12,
    				ModTime:  time.Unix(1360600916, 0),
    				Typeflag: TypeReg,
    			},
    			fm: 0644,
    		},
    		// hard link.
    		{
    			h: &Header{
    				Name:     "hard.txt",
    				Mode:     0644 | c_ISLNK,
    				Size:     0,
    				ModTime:  time.Unix(1360600916, 0),
    				Typeflag: TypeLink,
    			},
    			fm: 0644 | os.ModeSymlink,
    		},
    		// symbolic link.
    		{
    			h: &Header{
    				Name:     "link.txt",
    				Mode:     0777 | c_ISLNK,
    				Size:     0,
    				ModTime:  time.Unix(1360600852, 0),
    				Typeflag: TypeSymlink,
    			},
    			fm: 0777 | os.ModeSymlink,
    		},
    		// character device node.
    		{
    			h: &Header{
    				Name:     "dev/null",
    				Mode:     0666 | c_ISCHR,
    				Size:     0,
    				ModTime:  time.Unix(1360578951, 0),
    				Typeflag: TypeChar,
    			},
    			fm: 0666 | os.ModeDevice | os.ModeCharDevice,
    		},
    		// block device node.
    		{
    			h: &Header{
    				Name:     "dev/sda",
    				Mode:     0660 | c_ISBLK,
    				Size:     0,
    				ModTime:  time.Unix(1360578954, 0),
    				Typeflag: TypeBlock,
    			},
    			fm: 0660 | os.ModeDevice,
    		},
    		// directory.
    		{
    			h: &Header{
    				Name:     "dir/",
    				Mode:     0755 | c_ISDIR,
    				Size:     0,
    				ModTime:  time.Unix(1360601116, 0),
    				Typeflag: TypeDir,
    			},
    			fm: 0755 | os.ModeDir,
    		},
    		// fifo node.
    		{
    			h: &Header{
    				Name:     "dev/initctl",
    				Mode:     0600 | c_ISFIFO,
    				Size:     0,
    				ModTime:  time.Unix(1360578949, 0),
    				Typeflag: TypeFifo,
    			},
    			fm: 0600 | os.ModeNamedPipe,
    		},
    		// setuid.
    		{
    			h: &Header{
    				Name:     "bin/su",
    				Mode:     0755 | c_ISREG | c_ISUID,
    				Size:     23232,
    				ModTime:  time.Unix(1355405093, 0),
    				Typeflag: TypeReg,
    			},
    			fm: 0755 | os.ModeSetuid,
    		},
    		// setguid.
    		{
    			h: &Header{
    				Name:     "group.txt",
    				Mode:     0750 | c_ISREG | c_ISGID,
    				Size:     0,
    				ModTime:  time.Unix(1360602346, 0),
    				Typeflag: TypeReg,
    			},
    			fm: 0750 | os.ModeSetgid,
    		},
    		// sticky.
    		{
    			h: &Header{
    				Name:     "sticky.txt",
    				Mode:     0600 | c_ISREG | c_ISVTX,
    				Size:     7,
    				ModTime:  time.Unix(1360602540, 0),
    				Typeflag: TypeReg,
    			},
    			fm: 0600 | os.ModeSticky,
    		},
    	}
    
    	for i, g := range golden {
    		fi := g.h.FileInfo()
    		h2, err := FileInfoHeader(fi, "")
    		if err != nil {
    			t.Error(err)
    			continue
    		}
    		if got, want := h2.Name, g.h.Name; got != want {
    			t.Errorf("i=%d: Name: got %v, want %v", i, got, want)
    		}
    		if got, want := h2.Size, g.h.Size; got != want {
    			t.Errorf("i=%d: Size: got %v, want %v", i, got, want)
    		}
    		if got, want := h2.Mode, g.h.Mode; got != want {
    			t.Errorf("i=%d: Mode: got %o, want %o", i, got, want)
    		}
    		if got, want := fi.Mode(), g.fm; got != want {
    			t.Errorf("i=%d: fi.Mode: got %o, want %o", i, got, want)
    		}
    		if got, want := h2.ModTime, g.h.ModTime; got != want {
    			t.Errorf("i=%d: ModTime: got %v, want %v", i, got, want)
    		}
    		if sysh, ok := fi.Sys().(*Header); !ok || sysh != g.h {
    			t.Errorf("i=%d: Sys didn't return original *Header", i)
    		}
    	}
    }
    

コアとなるコードの解説

Header.FileInfo() メソッドと headerFileInfo 構造体

このコミットの最も重要な追加は、tar.Header 構造体に FileInfo() メソッドが導入されたことです。このメソッドは、os.FileInfo インターフェースを実装する headerFileInfo 型のインスタンスを返します。

  • headerFileInfo の役割: headerFileInfo は、tar.Header のデータを os.FileInfo が期待する形式に変換するアダプターとして機能します。これにより、Goの他のファイル操作関数が tar.Header を直接 os.FileInfo として受け入れられるようになり、コードの汎用性と再利用性が向上します。
  • Mode() メソッドのロジック: headerFileInfoMode() メソッドは、tar.HeaderMode フィールドと Typeflag フィールドを組み合わせて、完全な os.FileMode を構築します。
    • まず、os.FileMode(fi.h.Mode).Perm() で基本的なパーミッションビット(読み取り、書き込み、実行)を取得します。
    • 次に、fi.h.Modec_ISUID, c_ISGID, c_ISVTX とのビットAND演算によって、setuid, setgid, sticky ビットが設定されているかをチェックし、対応する os.ModeSetuid, os.ModeSetgid, os.ModeStickymode にOR演算で追加します。
    • さらに、fi.h.Mode からパーミッションビットと特殊ビットを除去した値 (m) を使用して、ファイルの種類(ディレクトリ、FIFO、シンボリックリンク、ブロックデバイス、キャラクターデバイス、ソケット)を判断し、対応する os.FileMode 定数を mode に追加します。
    • 最後に、fi.h.Typeflag (tarヘッダーのファイルタイプを示すフィールド) に基づいて、再度ファイルの種類をチェックし、mode に適切な os.FileMode 定数を追加します。これにより、Mode フィールドと Typeflag フィールドの両方からファイルモード情報を正確に抽出できます。

FileInfoHeader 関数の拡張

FileInfoHeader 関数は、os.FileInfo から tar.Header を生成する逆の変換を行います。このコミットでは、この関数が os.FileMode のより多くの種類を適切に tar.Header にマッピングできるように拡張されました。

  • fm := fi.Mode(): os.FileInfo から os.FileMode を一度取得し、fm 変数に格納することで、コードの重複を減らし、可読性を向上させています。
  • 新しい case の追加: switch ステートメントに case fm&os.ModeNamedPipe != 0: が追加され、os.ModeNamedPipe が設定されている場合に h.TypeflagTypeFifo に、h.Modec_ISFIFO を設定するようになりました。
  • 特殊パーミッションビットの処理: switch ステートメントの後に、fm&os.ModeSetuid != 0, fm&os.ModeSetgid != 0, fm&os.ModeSticky != 0 のチェックが追加されました。これにより、os.FileMode にこれらのビットが設定されている場合、対応する c_ISUID, c_ISGID, c_ISVTXh.Mode にOR演算で追加します。

これらの変更により、archive/tar パッケージは、Goのファイルシステム抽象化とtarアーカイブ形式の間で、より完全で正確なファイルメタデータの変換をサポートするようになりました。特に、Unix系システムで重要な意味を持つ特殊なパーミッションビットやファイルタイプが、Goプログラム内で適切に扱えるようになった点が大きな改善です。

関連リンク

参考にした情報源リンク