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

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

このコミットは、Go言語のgo/scannerパッケージにおけるバイトオーダーマーク(BOM)の処理に関する変更です。具体的には、ファイルの先頭にないBOMを不正なバイトオーダーマークとして拒否するようにスキャナーの挙動を修正しています。

コミット

commit 968732b677e592019526fe1afe0bbf45f52df4b7
Author: Robert Griesemer <gri@golang.org>
Date:   Fri Apr 12 21:28:38 2013 -0700

    go/scanner: reject BOMs that are not at the beginning
    
    For compliance with gc. See also issue 5265.
    Not Go1.1 critical, but harmless.
    
    R=r
    CC=golang-dev
    https://golang.org/cl/8736043

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

https://github.com/golang/go/commit/968732b677e592019526fe1afe0bbf45f52df4b7

元コミット内容

このコミットは、go/scannerパッケージにおいて、ファイルの先頭にないBOM(バイトオーダーマーク)をエラーとして扱うように変更します。これは、gcコンパイラの挙動に合わせるためのものであり、Go 1.1のリリースには必須ではないものの、無害な変更とされています。

変更の背景

Go言語のコンパイラであるgcは、ファイルの先頭にのみBOMを許可し、それ以外の位置にあるBOMをエラーとして扱います。しかし、go/scannerパッケージは、ファイルの先頭以外の位置にあるBOMを単に無視していました。この不一致は、Goのツールチェーン全体で一貫した挙動を保証する上で問題となります。

このコミットの目的は、go/scannergcと同じように、ファイルの先頭以外のBOMを不正な文字として扱うようにすることで、この不一致を解消することです。これにより、GoのソースコードがBOMを含む場合に、より予測可能で一貫したエラー処理が提供されます。

コミットメッセージには「issue 5265」への言及がありますが、Goの公式リポジトリでこの番号のIssueは見つかりませんでした。これは、内部的なトラッキング番号であるか、あるいは別のリポジトリのIssueを指している可能性があります。

前提知識の解説

バイトオーダーマーク (BOM)

バイトオーダーマーク(BOM)は、Unicodeテキストファイルの先頭に挿入される特殊な文字シーケンスで、そのファイルがどのUnicodeエンコーディング(UTF-8, UTF-16, UTF-32など)を使用しているか、またUTF-16やUTF-32の場合のバイト順(エンディアン)を示すために使用されます。

  • UTF-8 BOM: EF BB BF (16進数)。UTF-8はバイト順の概念がないため、BOMはエンコーディングの識別子としてのみ機能します。しかし、多くのUnix系ツールやプログラミング言語では、UTF-8ファイルにBOMが存在することを好ましくないと考えています。これは、BOMがファイルの先頭に現れると、スクリプトのshebang行(#!)やXML宣言などに影響を与える可能性があるためです。
  • UTF-16 BOM: FE FF (ビッグエンディアン) または FF FE (リトルエンディアン)。
  • UTF-32 BOM: 00 00 FE FF (ビッグエンディアン) または FF FE 00 00 (リトルエンディアン)。

Go言語のソースコードは通常UTF-8でエンコードされますが、BOMは必須ではありません。むしろ、Goのツールチェーンでは、BOMはファイルの先頭にのみ許可され、それ以外の場所ではエラーとして扱われるべきであるという方針が取られています。これは、BOMがコードの途中に現れると、予期せぬ構文エラーや解析エラーを引き起こす可能性があるためです。

go/scannerパッケージ

go/scannerパッケージは、Go言語のソースコードを字句解析(スキャン)するための機能を提供します。字句解析とは、ソースコードの文字列を、キーワード、識別子、演算子、リテラルなどの意味のあるトークン(字句)のシーケンスに変換するプロセスです。このパッケージは、Goコンパイラやその他のGoツール(go fmtなど)の基盤として使用されます。

スキャナーは、入力されたバイトストリームを読み込み、Unicode文字にデコードし、それらの文字を基にトークンを生成します。このコミットの変更は、スキャナーが文字を読み込む際に、BOMの出現位置をチェックし、不正なBOMを検出した場合にエラーを報告するように修正しています。

技術的詳細

このコミットは、go/scannerパッケージ内のScanner構造体と関連するメソッドに以下の変更を加えています。

  1. bom定数の追加: const bom = 0xFEFF UnicodeのBOM文字(U+FEFF)を表す定数が追加されました。これにより、コードの可読性が向上し、マジックナンバーの使用が避けられます。

  2. next()メソッドの変更: next()メソッドは、スキャナーが次のUnicode文字を読み込む際に呼び出されます。このメソッド内で、読み込んだ文字がBOMであり、かつそのBOMがファイルの先頭(s.offset > 0)ではない場合に、s.error()を呼び出して「illegal byte order mark」というエラーを報告するように変更されました。

    } else if r == bom && s.offset > 0 {
        s.error(s.offset, "illegal byte order mark")
    }
    

    s.offsetは、現在の文字がファイル内で何バイト目にあるかを示すオフセットです。s.offset > 0は、ファイルの先頭ではないことを意味します。

  3. Init()メソッドの変更: Init()メソッドは、スキャナーが初期化される際に呼び出されます。以前は、ファイルの先頭にBOMがある場合、それを無条件に無視していました。この挙動は維持されますが、コードのコメントが「ignore BOM」から「ignore BOM at file beginning」に変更され、より明確になりました。

    -	if s.ch == '\uFEFF' {
    -		s.next() // ignore BOM
    +	if s.ch == bom {
    +		s.next() // ignore BOM at file beginning
    	}
    

    これにより、ファイルの先頭のBOMは引き続き無視され、エラーにはなりません。

  4. scan()メソッドの変更: scan()メソッドは、個々のトークンをスキャンする主要なロジックを含んでいます。以前は、不正な文字が検出された場合、無条件にs.error()を呼び出していました。しかし、next()メソッドが既に不正なBOMを報告するようになったため、重複してエラーを報告しないように変更されました。

    		default:
    			// next reports unexpected BOMs - don't repeat
    			if ch != bom {
    				s.error(s.file.Offset(pos), fmt.Sprintf("illegal character %#U", ch))
    			}
    			insertSemi = s.insertSemi // preserve insertSemi info
    			tok = token.ILLEGAL
    			lit = string(ch)
    

    これにより、chがBOMでない場合にのみs.error()が呼び出されるようになり、同じBOMに対して二重にエラーが報告されるのを防ぎます。

  5. テストケースの追加/修正: scanner_test.goに、ファイルの先頭以外の位置にBOMが存在する場合のエラーをテストする新しいテストケースが追加されました。

    	{"\ufeff\ufeff", token.ILLEGAL, 3, "illegal byte order mark"},            // only first BOM is ignored
    	{"//\ufeff", token.COMMENT, 2, "illegal byte order mark"},                // only first BOM is ignored
    	{"'\ufeff" + `'`, token.CHAR, 1, "illegal byte order mark"},              // only first BOM is ignored
    	{`"` + "abc\ufeffdef" + `"`, token.STRING, 4, "illegal byte order mark"}, // only first BOM is ignored
    

    これらのテストケースは、コメント内、文字リテラル内、文字列リテラル内など、様々なコンテキストでBOMがファイルの先頭以外に現れた場合に、正しく「illegal byte order mark」エラーが報告されることを確認します。

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

src/pkg/go/scanner/scanner.go

--- a/src/pkg/go/scanner/scanner.go
+++ b/src/pkg/go/scanner/scanner.go
@@ -48,6 +48,8 @@ type Scanner struct {
 	ErrorCount int // number of errors encountered
 }
 
+const bom = 0xFEFF // byte order mark, only permitted as very first character
+
 // Read the next Unicode char into s.ch.
 // s.ch < 0 means end-of-file.
 //
@@ -67,6 +69,8 @@ func (s *Scanner) next() {
 		tr, w = utf8.DecodeRune(s.src[s.rdOffset:])
 		if r == utf8.RuneError && w == 1 {
 			s.error(s.offset, "illegal UTF-8 encoding")
+		} else if r == bom && s.offset > 0 {
+			s.error(s.offset, "illegal byte order mark")
 		}
 	}
 	s.rdOffset += w
@@ -125,8 +129,8 @@ func (s *Scanner) Init(file *token.File, src []byte, err ErrorHandler, mode Mode
 	s.ErrorCount = 0
 
 	s.next()
-	if s.ch == '\uFEFF' {
-		s.next() // ignore BOM
+	if s.ch == bom {
+		s.next() // ignore BOM at file beginning
 	}
 }
 
@@ -713,7 +717,10 @@ scanAgain:
 		case '|':
 			tok = s.switch3(token.OR, token.OR_ASSIGN, '|', token.LOR)
 		default:
-			s.error(s.file.Offset(pos), fmt.Sprintf("illegal character %#U", ch))
+			// next reports unexpected BOMs - don't repeat
+			if ch != bom {
+				s.error(s.file.Offset(pos), fmt.Sprintf("illegal character %#U", ch))
+			}
 			insertSemi = s.insertSemi // preserve insertSemi info
 			tok = token.ILLEGAL
 			lit = string(ch)

src/pkg/go/scanner/scanner_test.go

--- a/src/pkg/go/scanner/scanner_test.go
+++ b/src/pkg/go/scanner/scanner_test.go
@@ -695,7 +695,10 @@ var errors = []struct {
 	{"0X", token.INT, 0, "illegal hexadecimal number"},
 	{"\"abc\x00def\"", token.STRING, 4, "illegal character NUL"},
 	{"\"abc\x80def\"", token.STRING, 4, "illegal UTF-8 encoding"},
-	{"\ufeff\ufeff", token.ILLEGAL, 3, "illegal character U+FEFF"}, // only first BOM is ignored
+	{"\ufeff\ufeff", token.ILLEGAL, 3, "illegal byte order mark"},            // only first BOM is ignored
+	{"//\ufeff", token.COMMENT, 2, "illegal byte order mark"},                // only first BOM is ignored
+	{"'\ufeff" + `'`, token.CHAR, 1, "illegal byte order mark"},              // only first BOM is ignored
+	{`"` + "abc\ufeffdef" + `"`, token.STRING, 4, "illegal byte order mark"}, // only first BOM is ignored
 }
 
 func TestScanErrors(t *testing.T) {

コアとなるコードの解説

このコミットの核心は、go/scannerがBOMを扱う方法を、Goのコンパイラgcの挙動と一致させることです。

  1. bom定数: 0xFEFFというマジックナンバーをbomという定数に置き換えることで、コードの意図が明確になり、将来的な変更やメンテナンスが容易になります。

  2. next()メソッドのBOMチェック: next()メソッドは、ソースコードの次のUnicode文字を読み込むたびに実行されます。このメソッド内で、読み込んだ文字rbom(U+FEFF)であり、かつs.offset0より大きい(つまり、ファイルの先頭ではない)場合に、s.error()を呼び出してエラーを報告します。これにより、ファイルの途中にBOMが挿入された場合に、スキャナーがそれを不正な文字として認識し、適切なエラーメッセージを生成するようになります。

  3. Init()メソッドのBOM処理: Init()メソッドは、スキャナーがソースファイルを読み込み始める際に一度だけ呼び出されます。ここでは、ファイルの先頭にBOMが存在する場合(s.ch == bom)、そのBOMを読み飛ばして(s.next())無視します。この挙動は変更されていませんが、コメントが「ignore BOM at file beginning」と修正され、ファイルの先頭のBOMのみが特別扱いされることが明確になりました。

  4. scan()メソッドのエラー重複防止: scan()メソッドは、個々のトークンを識別するロジックを含んでいます。以前は、認識できない文字(defaultケース)に遭遇した場合、一律に「illegal character」エラーを報告していました。しかし、next()メソッドが既に不正なBOMを検出してエラーを報告するようになったため、scan()メソッドが同じBOMに対して再度エラーを報告するのを避けるために、if ch != bomという条件が追加されました。これにより、エラーメッセージの重複を防ぎ、よりクリーンなエラー報告を実現します。

これらの変更により、go/scannerは、GoのソースコードにおけるBOMの取り扱いに関して、gcコンパイラと一貫した厳格なポリシーを適用するようになります。これにより、Goのツールチェーン全体での挙動の予測可能性と信頼性が向上します。

関連リンク

参考にした情報源リンク