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

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

このコミットは、Go言語の初期開発段階における go/pretty ツール(コード整形ツール)のHTML出力機能に関する重要な変更を記録しています。具体的には、HTMLタグの生成とエスケープ処理を簡素化し、より正確なHTMLページをソースコードから生成できるようにするためのリファクタリングが行われました。これにより、独立した htmlwriter パッケージが廃止され、その機能が printer パッケージに直接統合されました。

コミット

commit 4873bb217c18b838feb12c58200b6a02e49c906c
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Dec 10 13:51:19 2008 -0800

    Snapshot:
    - fixed html tag generation
    - simplified html escaping machinery
      (not 100% correct for strings yet)
    - can now produce the first mostly correct formatted html pages from source
      with (fake) links: e.g. pretty -html srcfile.go > srcfile.html
      R=r
    
    OCL=20915
    CL=20915

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

https://github.com/golang/go/commit/4873bb217c18b838feb12c58200b6a02e49c906c

元コミット内容

このコミットは、go/pretty ツールにおけるHTML出力機能のスナップショット更新です。主な変更点は以下の通りです。

  • HTMLタグの生成が修正されました。
  • HTMLエスケープの仕組みが簡素化されました(ただし、文字列に対するエスケープはまだ100%正確ではないとされています)。
  • ソースコードから、ほぼ正確に整形されたHTMLページ(擬似的なリンクを含む)を生成できるようになりました。例えば、pretty -html srcfile.go > srcfile.html のように実行できます。

変更の背景

このコミットが行われた2008年12月は、Go言語がまだ一般に公開される前の非常に初期の開発段階でした。go/pretty ツールは、Goのソースコードを整形し、場合によってはHTML形式で出力する機能を持っていました。

初期の実装では、HTMLの生成とエスケープ処理が htmlwriter という独立したパッケージで行われていました。しかし、このような分離は、HTML生成ロジックとコード整形ロジックの間の密接な連携を妨げ、複雑さを増していた可能性があります。特に、HTMLエスケープはセキュリティ(クロスサイトスクリプティング (XSS) 攻撃の防止)と表示の正確性の両方において非常に重要であり、その処理が不完全であったり、扱いにくかったりすると問題が生じます。

このコミットの背景には、以下の目的があったと考えられます。

  1. コードの簡素化と統合: htmlwriter パッケージの機能を printer パッケージに直接取り込むことで、HTML生成とエスケープのロジックを一元化し、コードベース全体の理解と保守を容易にする。
  2. HTML出力の品質向上: HTMLタグの生成におけるバグを修正し、エスケープ処理を改善することで、より正確でブラウザで正しく表示されるHTML出力を実現する。
  3. 開発効率の向上: pretty ツールがより信頼性の高いHTML出力を提供できるようになることで、Go言語のソースコードをWeb上で閲覧する際の利便性を高め、開発者がコードを共有・レビューしやすくする。

コミットメッセージにある「(not 100% correct for strings yet)」という記述は、この時点ではまだHTMLエスケープ処理が完璧ではないことを示しており、継続的な改善の途上であったことを示唆しています。

前提知識の解説

このコミットを理解するためには、以下の概念やGo言語の初期の文脈に関する知識が役立ちます。

  1. Go言語の初期開発: Go言語は2009年11月に一般公開されましたが、このコミットはそれ以前の2008年に行われています。当時のGo言語はまだ実験的な段階であり、APIやパッケージ構造は流動的でした。usr/gri/pretty のようなパスは、開発者の個人作業ディレクトリを示している可能性があり、現在のGoの標準ライブラリの構造とは異なります。
  2. go/pretty ツール: これはGo言語のソースコードを整形(pretty-print)するためのツールです。コードの可読性を高めるために、インデント、改行、スペースなどを統一する役割を担います。また、このコミットが示すように、HTML形式での出力機能も持っていました。
  3. HTMLエスケープ: HTML文書内で特殊な意味を持つ文字(例: <>&"')を、それらの文字自体として表示するために、対応するHTMLエンティティ(例: &lt;&gt;&amp;&quot;&#39;)に変換する処理です。これは、ブラウザがHTMLタグとして解釈するのを防ぎ、XSS(クロスサイトスクリプティング)などのセキュリティ脆弱性を防ぐ上で不可欠です。
  4. Makefile: make ユーティリティが使用する設定ファイルで、ソフトウェアのビルドプロセスを自動化するために使われます。依存関係を定義し、それらを解決するためのコマンドを記述します。このコミットでは、printer.6 のビルド依存関係から htmlwriter.6 が削除されています。.6 拡張子は、当時のGoコンパイラが生成するオブジェクトファイルやパッケージファイルを示している可能性があります。
  5. io.Writer インターフェース: Go言語の標準ライブラリ io パッケージで定義されているインターフェースで、データを書き込むための抽象化を提供します。Write([]byte) (n int, err error) メソッドを持ち、ファイル、ネットワーク接続、メモリバッファなど、様々な出力先に統一的にデータを書き込むことができます。
  6. tabwriter パッケージ: Go言語の標準ライブラリ text/tabwriter パッケージ(当時は tabwriter として直接インポートされていた可能性)は、テキストをタブ区切りで整形し、列を揃えるための機能を提供します。このコミットでは、pretty ツールが整形されたテキストをHTML出力に利用していることが示唆されます。

技術的詳細

このコミットの技術的な核心は、HTMLエスケープとHTMLタグ生成のロジックを、独立した htmlwriter パッケージから printer パッケージに直接移行し、より統合されたアプローチを採用した点にあります。

  1. htmlwriter.go の削除:

    • usr/gri/pretty/htmlwriter.go ファイルが完全に削除されました。これは、HTMLエスケープとタグ生成の責任がこのファイルから他の場所へ移されたことを意味します。
    • Makefile からも printer.6 の依存関係として htmlwriter.6 が削除され、ビルドシステムレベルでの分離が完了しました。
  2. printer.go への機能統合:

    • printer.go から htmlwriter パッケージのインポートが削除されました。
    • Printer 構造体から tags *htmlwriter.Writer フィールドが削除され、Printerhtmlwriter に直接依存しなくなりました。
    • Printer.Init メソッドのシグネチャが変更され、tags *htmlwriter.Writer 引数が削除されました。これにより、Printer の初期化時に htmlwriter のインスタンスを渡す必要がなくなりました。
  3. HtmlEscape 関数の導入:

    • printer.go 内に HtmlEscape(s string) string という新しい関数が追加されました。この関数は、入力文字列 s 内のHTML特殊文字(<&)を対応するHTMLエンティティ(&lt;&amp;)に再帰的に変換します。
    • 注目すべきは、html.BVal() というフラグがチェックされている点です。これは、HTML出力モードが有効な場合にのみエスケープ処理が行われることを示唆しています。
    • 実装は再帰的であり、文字列を走査し、特殊文字が見つかった場合にその文字をエスケープし、残りの文字列に対して再帰呼び出しを行うというものです。これは、当時のGo言語の文字列操作や再帰の典型的なパターンを示しています。
  4. HTMLタグ生成ロジックの変更:

    • 以前は P.tags.Tag(...) を使用してHTMLタグを生成していましたが、これが P.TaggedString(pos, tag, s, endtag string) メソッドの呼び出しに置き換えられました。
    • TaggedString メソッドは、開始タグ、エスケープされた文字列、終了タグをまとめて出力する汎用的な機能を提供します。これにより、HTMLタグとコンテンツの出力がより密接に連携するようになりました。
    • HtmlPrologue (HTMLヘッダとボディの開始) および HtmlEpilogue (HTMLボディとドキュメントの終了) メソッドも P.tags.Tag から P.TaggedString を使用するように変更されました。特に、HtmlPrologue では titleHtmlEscape されるようになりました。
    • HtmlIdentifier メソッドも同様に P.TaggedString を使用するように変更され、識別子自体はエスケープの必要がないことがコメントで明記されています。
  5. 文字列出力の変更:

    • P.String(pos int, s string) メソッドが、内部的に P.TaggedString(pos, "", s, "") を呼び出すように変更されました。これは、通常の文字列出力も TaggedString の汎用的なフレームワークに乗せることで、コードの一貫性を高める意図があったと考えられます。
    • コメントの出力部分 (ctext) も HtmlEscape(ctext) を通すように変更され、コメント内の特殊文字がHTMLで正しく表示されるようになりました。
    • Printf の代わりに String(0, ";") を使用する箇所が追加され、HTML出力モードでのセミコロンの扱いが統一されました。
  6. Print 関数の変更:

    • Print 関数内で htmlwriter.New(text) を呼び出して tags インスタンスを生成していた部分が削除されました。これは、htmlwriter パッケージが不要になったことを直接的に示しています。
    • P.Init の呼び出しも、tags 引数が削除された新しいシグネチャに合わせて変更されました。
  7. test.sh の変更:

    • usr/gri/pretty/test.shtypes_amd64_darwin.go がスキップ対象ファイルとして追加されました。これは、この特定のファイルが当時の pretty ツールの整形またはHTML出力テストで問題を引き起こしていたことを示唆しています。

これらの変更は、Go言語のコード整形ツールが、より堅牢で統合されたHTML出力機能を持つように進化する過程を示しています。特に、HTMLエスケープのロジックをコアの printer パッケージに直接組み込むことで、将来的な改善や機能拡張が容易になる基盤を築いたと言えます。

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

  • usr/gri/pretty/Makefile:
    • printer.6 の依存関係から htmlwriter.6 を削除。
  • usr/gri/pretty/htmlwriter.go:
    • ファイル全体が削除。
  • usr/gri/pretty/printer.go:
    • import "htmlwriter"; の削除。
    • type Printer struct から tags *htmlwriter.Writer; フィールドの削除。
    • func (P *Printer) Init(...) のシグネチャ変更(tags *htmlwriter.Writer 引数の削除)。
    • 新関数 func HtmlEscape(s string) string の追加。
    • func (P *Printer) String(pos int, s string)func (P *Printer) TaggedString(pos int, tag, s, endtag string) に変更され、旧 StringTaggedString を呼び出すラッパーに。
    • Printf やコメント出力部分で HtmlEscape の使用を開始。
    • HtmlPrologue, HtmlEpilogue, HtmlIdentifier メソッドの内部実装が P.tags.Tag から P.TaggedString を使用するように変更。
    • Print 関数内で htmlwriter.New の呼び出しを削除し、P.Init の引数を変更。
  • usr/gri/pretty/test.sh:
    • スキップ対象ファイルリストに types_amd64_darwin.go を追加。

コアとなるコードの解説

usr/gri/pretty/Makefile の変更

--- a/usr/gri/pretty/Makefile
+++ b/usr/gri/pretty/Makefile
@@ -37,7 +37,7 @@ parser.6:	 scanner.6 ast.6
 
 platform.6:	 utils.6
 
-printer.6:	 scanner.6 ast.6 htmlwriter.6
+printer.6:	 scanner.6 ast.6
 
 %.6:	%.go
 	$(G) $(F) $<

printer.6 のビルドに必要な依存関係から htmlwriter.6 が削除されました。これは、printer パッケージが htmlwriter パッケージに依存しなくなったことをビルドシステムに伝えています。これにより、htmlwriter.go ファイルの削除と整合性が取れます。

usr/gri/pretty/htmlwriter.go の削除

--- a/usr/gri/pretty/htmlwriter.go
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright 2009 The Go Authors.  All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package htmlwriter
-
-import (
-	"os";
-	"io";
-	"fmt";
-)
-
-// Writer is a filter implementing the io.Write interface.
-// It provides facilities to generate HTML tags and does
-// HTML-escaping for text written through Write. Incoming
-// text is assumed to be UTF-8 encoded.
-
-export type Writer struct {
-	// TODO should not export any of the fields
-	writer io.Write;
-}
-
-
-func (b *Writer) Init(writer io.Write) *Writer {
-	b.writer = writer;
-	return b;
-}
-
-
-/* export */ func (p *Writer) Write(buf *[]byte) (written int, err *os.Error) {
-	i0 := 0;
-	for i := i0; i < len(buf); i++ {
-		var s string;
-		switch buf[i] {
-		case '<': s = "&lt;";
-		case '&': s = "&amp;";
-		default: continue;
-		}
-		// write HTML escape instead of buf[i]
-		w1, e1 := p.writer.Write(buf[i0 : i]);
-		if e1 != nil {
-			return i0 + w1, e1;
-		}
-		w2, e2 := io.WriteString(p.writer, s);
-		if e2 != nil {
-			return i0 + w1 /* not w2! */, e2;
-		}
-		i0 = i + 1;
-	}
-	written, err = p.writer.Write(buf[i0 : len(buf)]);
-	return len(buf), err;
-}
-
-
-// ----------------------------------------------------------------------------
-// HTML-specific interface
-
-/* export */ func (p *Writer) Tag(s string) {
-	// TODO proper error handling
-	io.WriteString(p.writer, s);
-}
-
-
-// ----------------------------------------------------------------------------
-//
-
-export func New(writer io.Write) *Writer {
-	return new(Writer).Init(writer);
-}

htmlwriter.go は、HTMLエスケープとタグ生成の機能を提供していた独立したパッケージでした。このファイルの削除は、これらの機能が printer パッケージに直接統合されたことを明確に示しています。これにより、コードベースのモジュール構成が変更され、HTML出力に関するロジックがより一元化されました。

usr/gri/pretty/printer.go の変更

インポートと構造体の変更

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -11,7 +11,6 @@ import (
 	"tabwriter";
 	"flag";
 	"fmt";
-	"htmlwriter";
 	Scanner "scanner";
 	AST "ast";
 )
@@ -57,7 +56,6 @@ const (
 type Printer struct {
 	// output
 	text io.Write;
-	tags *htmlwriter.Writer;
 	
 	// comments
 	comments *array.Array;  // the list of all comments
@@ -94,10 +92,9 @@ func (P *Printer) NextComment() {
 }
 
 
-func (P *Printer) Init(text io.Write, tags *htmlwriter.Writer, comments *array.Array) {
+func (P *Printer) Init(text io.Write, comments *array.Array) {
 	// writers
 	P.text = text;
-	P.tags = tags;
 	
 	// comments
 	P.comments = comments;

htmlwriter パッケージのインポートが削除され、Printer 構造体から tags *htmlwriter.Writer フィールドが削除されました。これに伴い、Printer.Init メソッドのシグネチャも変更され、htmlwriter.Writer のインスタンスを受け取らなくなりました。これは、Printer がHTMLエスケープとタグ生成の責任を直接負うようになったことを示しています。

HtmlEscape 関数の追加

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -111,6 +108,22 @@ func (P *Printer) Init(text io.Write, tags *htmlwriter.Writer, comments *array.A
 // ----------------------------------------------------------------------------
 // Printing support
 
+func HtmlEscape(s string) string {
+	if html.BVal() {
+		var esc string;
+		for i := 0; i < len(s); i++ {
+			switch s[i] {
+			case '<': esc = "&lt";
+			case '&': esc = "&amp";
+			default: continue;
+			}
+			return s[0 : i] + esc + HtmlEscape(s[i+1 : len(s)]);
+		}
+	}
+	return s;
+}
+
+
 func (P *Printer) Printf(format string, s ...) {
 	n, err := fmt.fprintf(P.text, format, s);
 	if err != nil {

HtmlEscape 関数が追加されました。この関数は、HTML出力モード (html.BVal()) が有効な場合に、入力文字列 s 内の <& をそれぞれ &lt;&amp; に変換します。再帰的な実装により、文字列全体を走査し、特殊文字をエスケープします。この関数が printer パッケージ内に直接定義されたことで、HTMLエスケープ処理がより密接にコード整形ロジックと連携するようになりました。

String および TaggedString メソッドの変更

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -135,7 +148,7 @@ func (P *Printer) Newline(n int) {
 }\n \n \n-func (P *Printer) String(pos int, s string) {\n+func (P *Printer) TaggedString(pos int, tag, s, endtag string) {\n \t// use estimate for pos if we don\'t have one\n \tif pos == 0 {\n \t\tpos = P.lastpos;\n@@ -230,7 +243,7 @@ func (P *Printer) String(pos int, s string) {\n \t\t\tif debug.BVal() {\n \t\t\t\tP.Printf(\"[%d]\", P.cpos);\n \t\t\t}\n-\t\t\tP.Printf(\"%s\", ctext);\n+\t\t\tP.Printf(\"%s\", HtmlEscape(ctext));\n \n \t\t\tif ctext[1] == \'/\' {\n \t\t\t\t//-style comments must end in newline\n@@ -276,7 +289,7 @@ func (P *Printer) String(pos int, s string) {\n \tif debug.BVal() {\n \t\tP.Printf(\"[%d]\", pos);\n \t}\n-\tP.Printf(\"%s\", s);\n+\tP.Printf(\"%s%s%s\", tag, HtmlEscape(s), endtag);\n \n \t// --------------------------------\n \t// interpret state\n@@ -300,6 +313,11 @@ func (P *Printer) String(pos int, s string) {\n }\n \n \n+func (P *Printer) String(pos int, s string) {\n+\tP.TaggedString(pos, \"\", s, \"\");\n+}\n+\n+\n func (P *Printer) Token(pos int, tok int) {\n \tP.String(pos, Scanner.TokenString(tok));\n }\n```
`String` メソッドが `TaggedString` にリネームされ、開始タグ、文字列、終了タグを受け取るようになりました。これにより、HTMLタグで囲まれたコンテンツをより柔軟に出力できるようになります。元の `String` メソッドは、新しい `TaggedString` を呼び出すラッパーとして再定義され、既存のコードとの互換性を保っています。コメントのテキスト (`ctext`) や通常の文字列出力も `HtmlEscape` を通すようになり、HTML出力時の安全性が向上しました。

#### HTML関連メソッドの変更

```diff
--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -316,36 +334,39 @@ func (P *Printer) Error(pos int, tok int, msg string) {\n // HTML support\n \n func (P *Printer) HtmlPrologue(title string) {\n-\tif P.tags != nil {\n-\t\tP.tags.Tag(\n+\tif html.BVal() {\n+\t\tP.TaggedString(0,\n \t\t\t"<html>\\n"\n \t\t\t"<head>\\n"\n \t\t\t"\t<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\\n"\n-\t\t\t"\t<title>" + title + "</title>\\n"\n+\t\t\t"\t<title>" + HtmlEscape(title) + "</title>\\n"\n \t\t\t"\t<style type=\"text/css\">\\n"\n \t\t\t"\t</style>\\n"\n \t\t\t"</head>\\n"\n \t\t\t"<body>\\n"\n-\t\t\t"<pre>\\n"\n+\t\t\t"<pre>\\n",\n+\t\t\t"", ""\n \t\t)\n \t}\n }\n \n \n func (P *Printer) HtmlEpilogue() {\n-\tif P.tags != nil {\n-\t\tP.tags.Tag(\n+\tif html.BVal() {\n+\t\tP.TaggedString(0, \n \t\t\t"</pre>\\n"\n \t\t\t"</body>\\n"\n-\t\t\t"<html>\\n"\n+\t\t\t"<html>\\n",\n+\t\t\t"", ""\n \t\t)\n \t}\n }\n \n \n func (P *Printer) HtmlIdentifier(pos int, ident string) {\n-\tif false && P.tags != nil {\n-\t\tP.tags.Tag(`<a href="#` + ident + `">` + ident + `</a>`);\n+\tif html.BVal() {\n+\t\t// no need to HtmlEscape ident\n+\t\tP.TaggedString(pos, `<a href="#` + ident + `">`, ident, `</a>`);\n \t} else {\n \t\tP.String(pos, ident);\n \t}\n```
`HtmlPrologue`, `HtmlEpilogue`, `HtmlIdentifier` の各メソッドも、`P.tags.Tag` の代わりに新しく導入された `P.TaggedString` を使用するように変更されました。これにより、HTMLタグの生成が `printer` パッケージ内で一貫して処理されるようになりました。特に、`HtmlPrologue` ではタイトルも `HtmlEscape` されるようになり、セキュリティが向上しています。

#### `Print` 関数の変更

```diff
--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -838,14 +859,8 @@ export func Print(prog *AST.Program) {\n \tif usetabs.BVal() {\n \t\tpadchar = '\t';\n \t}\n-\tvar (\n-\t\ttext = tabwriter.New(os.Stdout, int(tabwidth.IVal()), 1, padchar, true, html.BVal());\n-\t\ttags *htmlwriter.Writer;\n-\t)\n-\tif html.BVal() {\n-\t\ttags = htmlwriter.New(text);\n-\t}\n-\tP.Init(text, tags, prog.comments);\n+\ttext := tabwriter.New(os.Stdout, int(tabwidth.IVal()), 1, padchar, true, html.BVal());\n+\tP.Init(text, prog.comments);\n \n \tP.HtmlPrologue("<the source>");\n \tP.Program(prog);\

Print 関数は、pretty ツールのエントリポイントの一つです。この変更では、htmlwriter.New を呼び出して tags インスタンスを生成するロジックが完全に削除されました。これは、htmlwriter パッケージが不要になったことを最終的に示しています。P.Init の呼び出しも、tags 引数が削除された新しいシグネチャに合わせて更新されています。

usr/gri/pretty/test.sh の変更

--- a/usr/gri/pretty/test.sh
+++ b/usr/gri/pretty/test.sh
@@ -23,7 +23,7 @@ apply1() {\n 	#echo $1 $2\n 	case `basename $F` in\n 	# these files don\'t pass the idempotency test yet\n-\tlog.go | type.go | \\\n+\tlog.go | type.go | types_amd64_darwin.go | \\\
 \t\\\
 \tselftest1.go | func3.go | bug014.go | bug029.go | bug032.go | bug050.go | \\\
 \tbug068.go | bug088.go | bug083.go | bug106.go | bug125.go ) ;;  # skip - files contain syntax errors

test.sh スクリプトは、pretty ツールのテストを実行するためのものです。この変更では、types_amd64_darwin.go が、まだ冪等性テスト(整形しても結果が変わらないこと)をパスしないファイルとしてスキップリストに追加されました。これは、この特定のファイルが、新しいHTML出力ロジックや整形ロジックの変更によって、期待通りの結果を生成していなかった可能性を示唆しています。

関連リンク

  • Go言語公式サイト: https://go.dev/
  • Go言語の初期の歴史に関する情報(非公式リソースを含む)

参考にした情報源リンク

  • Go言語の公式ドキュメント(現在のバージョン)
  • HTMLエスケープに関する一般的な情報(OWASPなど)
  • Gitのコミットログと差分表示の解釈に関する知識
  • Makefile の基本的な構文と使用法
  • Go言語の io パッケージと text/tabwriter パッケージのドキュメント(当時のバージョンに相当する概念)
  • Go言語の初期開発に関するコミュニティの議論やブログ記事(もしあれば)I have provided the detailed explanation as requested, following all the specified instructions and chapter structure. I have used the commit data and the provided metadata, and incorporated technical details and background information. The output is in Markdown format and printed to standard output only.```markdown

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

このコミットは、Go言語の初期開発段階における go/pretty ツール(コード整形ツール)のHTML出力機能に関する重要な変更を記録しています。具体的には、HTMLタグの生成とエスケープ処理を簡素化し、より正確なHTMLページをソースコードから生成できるようにするためのリファクタリングが行われました。これにより、独立した htmlwriter パッケージが廃止され、その機能が printer パッケージに直接統合されました。

コミット

commit 4873bb217c18b838feb12c58200b6a02e49c906c
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Dec 10 13:51:19 2008 -0800

    Snapshot:
    - fixed html tag generation
    - simplified html escaping machinery
      (not 100% correct for strings yet)
    - can now produce the first mostly correct formatted html pages from source
      with (fake) links: e.g. pretty -html srcfile.go > srcfile.html
      R=r
    
    OCL=20915
    CL=20915

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

https://github.com/golang/go/commit/4873bb217c18b838feb12c58200b6a02e49c906c

元コミット内容

このコミットは、go/pretty ツールにおけるHTML出力機能のスナップショット更新です。主な変更点は以下の通りです。

  • HTMLタグの生成が修正されました。
  • HTMLエスケープの仕組みが簡素化されました(ただし、文字列に対するエスケープはまだ100%正確ではないとされています)。
  • ソースコードから、ほぼ正確に整形されたHTMLページ(擬似的なリンクを含む)を生成できるようになりました。例えば、pretty -html srcfile.go > srcfile.html のように実行できます。

変更の背景

このコミットが行われた2008年12月は、Go言語がまだ一般に公開される前の非常に初期の開発段階でした。go/pretty ツールは、Goのソースコードを整形し、場合によってはHTML形式で出力する機能を持っていました。

初期の実装では、HTMLの生成とエスケープ処理が htmlwriter という独立したパッケージで行われていました。しかし、このような分離は、HTML生成ロジックとコード整形ロジックの間の密接な連携を妨げ、複雑さを増していた可能性があります。特に、HTMLエスケープはセキュリティ(クロスサイトスクリプティング (XSS) 攻撃の防止)と表示の正確性の両方において非常に重要であり、その処理が不完全であったり、扱いにくかったりすると問題が生じます。

このコミットの背景には、以下の目的があったと考えられます。

  1. コードの簡素化と統合: htmlwriter パッケージの機能を printer パッケージに直接取り込むことで、HTML生成とエスケープのロジックを一元化し、コードベース全体の理解と保守を容易にする。
  2. HTML出力の品質向上: HTMLタグの生成におけるバグを修正し、エスケープ処理を改善することで、より正確でブラウザで正しく表示されるHTML出力を実現する。
  3. 開発効率の向上: pretty ツールがより信頼性の高いHTML出力を提供できるようになることで、Go言語のソースコードをWeb上で閲覧する際の利便性を高め、開発者がコードを共有・レビューしやすくする。

コミットメッセージにある「(not 100% correct for strings yet)」という記述は、この時点ではまだHTMLエスケープ処理が完璧ではないことを示しており、継続的な改善の途上であったことを示唆しています。

前提知識の解説

このコミットを理解するためには、以下の概念やGo言語の初期の文脈に関する知識が役立ちます。

  1. Go言語の初期開発: Go言語は2009年11月に一般公開されましたが、このコミットはそれ以前の2008年に行われています。当時のGo言語はまだ実験的な段階であり、APIやパッケージ構造は流動的でした。usr/gri/pretty のようなパスは、開発者の個人作業ディレクトリを示している可能性があり、現在のGoの標準ライブラリの構造とは異なります。
  2. go/pretty ツール: これはGo言語のソースコードを整形(pretty-print)するためのツールです。コードの可読性を高めるために、インデント、改行、スペースなどを統一する役割を担います。また、このコミットが示すように、HTML形式での出力機能も持っていました。
  3. HTMLエスケープ: HTML文書内で特殊な意味を持つ文字(例: <>&"')を、それらの文字自体として表示するために、対応するHTMLエンティティ(例: &lt;&gt;&amp;&quot;&#39;)に変換する処理です。これは、ブラウザがHTMLタグとして解釈するのを防ぎ、XSS(クロスサイトスクリプティング)などのセキュリティ脆弱性を防ぐ上で不可欠です。
  4. Makefile: make ユーティリティが使用する設定ファイルで、ソフトウェアのビルドプロセスを自動化するために使われます。依存関係を定義し、それらを解決するためのコマンドを記述します。このコミットでは、printer.6 のビルド依存関係から htmlwriter.6 が削除されています。.6 拡張子は、当時のGoコンパイラが生成するオブジェクトファイルやパッケージファイルを示している可能性があります。
  5. io.Writer インターフェース: Go言語の標準ライブラリ io パッケージで定義されているインターフェースで、データを書き込むための抽象化を提供します。Write([]byte) (n int, err error) メソッドを持ち、ファイル、ネットワーク接続、メモリバッファなど、様々な出力先に統一的にデータを書き込むことができます。
  6. tabwriter パッケージ: Go言語の標準ライブラリ text/tabwriter パッケージ(当時は tabwriter として直接インポートされていた可能性)は、テキストをタブ区切りで整形し、列を揃えるための機能を提供します。このコミットでは、pretty ツールが整形されたテキストをHTML出力に利用していることが示唆されます。

技術的詳細

このコミットの技術的な核心は、HTMLエスケープとHTMLタグ生成のロジックを、独立した htmlwriter パッケージから printer パッケージに直接移行し、より統合されたアプローチを採用した点にあります。

  1. htmlwriter.go の削除:

    • usr/gri/pretty/htmlwriter.go ファイルが完全に削除されました。これは、HTMLエスケープとタグ生成の責任がこのファイルから他の場所へ移されたことを意味します。
    • Makefile からも printer.6 の依存関係として htmlwriter.6 が削除され、ビルドシステムレベルでの分離が完了しました。
  2. printer.go への機能統合:

    • printer.go から htmlwriter パッケージのインポートが削除されました。
    • Printer 構造体から tags *htmlwriter.Writer フィールドが削除され、Printerhtmlwriter に直接依存しなくなりました。
    • Printer.Init メソッドのシグネチャが変更され、tags *htmlwriter.Writer 引数が削除されました。これにより、Printer の初期化時に htmlwriter のインスタンスを渡す必要がなくなりました。
  3. HtmlEscape 関数の導入:

    • printer.go 内に HtmlEscape(s string) string という新しい関数が追加されました。この関数は、入力文字列 s 内のHTML特殊文字(<&)を対応するHTMLエンティティ(&lt;&amp;)に再帰的に変換します。
    • 注目すべきは、html.BVal() というフラグがチェックされている点です。これは、HTML出力モードが有効な場合にのみエスケープ処理が行われることを示唆しています。
    • 実装は再帰的であり、文字列を走査し、特殊文字が見つかった場合にその文字をエスケープし、残りの文字列に対して再帰呼び出しを行うというものです。これは、当時のGo言語の文字列操作や再帰の典型的なパターンを示しています。
  4. HTMLタグ生成ロジックの変更:

    • 以前は P.tags.Tag(...) を使用してHTMLタグを生成していましたが、これが P.TaggedString(pos, tag, s, endtag string) メソッドの呼び出しに置き換えられました。
    • TaggedString メソッドは、開始タグ、エスケープされた文字列、終了タグをまとめて出力する汎用的な機能を提供します。これにより、HTMLタグとコンテンツの出力がより密接に連携するようになりました。
    • HtmlPrologue (HTMLヘッダとボディの開始) および HtmlEpilogue (HTMLボディとドキュメントの終了) メソッドも P.tags.Tag から P.TaggedString を使用するように変更されました。特に、HtmlPrologue では titleHtmlEscape されるようになりました。
    • HtmlIdentifier メソッドも同様に P.TaggedString を使用するように変更され、識別子自体はエスケープの必要がないことがコメントで明記されています。
  5. 文字列出力の変更:

    • P.String(pos int, s string) メソッドが、内部的に P.TaggedString(pos, "", s, "") を呼び出すように変更されました。これは、通常の文字列出力も TaggedString の汎用的なフレームワークに乗せることで、コードの一貫性を高める意図があったと考えられます。
    • コメントの出力部分 (ctext) も HtmlEscape(ctext) を通すように変更され、コメント内の特殊文字がHTMLで正しく表示されるようになりました。
    • Printf の代わりに String(0, ";") を使用する箇所が追加され、HTML出力モードでのセミコロンの扱いが統一されました。
  6. Print 関数の変更:

    • Print 関数内で htmlwriter.New(text) を呼び出して tags インスタンスを生成していた部分が削除されました。これは、htmlwriter パッケージが不要になったことを最終的に示しています。
    • P.Init の呼び出しも、tags 引数が削除された新しいシグネチャに合わせて変更されました。
  7. test.sh の変更:

    • usr/gri/pretty/test.shtypes_amd64_darwin.go がスキップ対象ファイルとして追加されました。これは、この特定のファイルが当時の pretty ツールの整形またはHTML出力テストで問題を引き起こしていたことを示唆しています。

これらの変更は、Go言語のコード整形ツールが、より堅牢で統合されたHTML出力機能を持つように進化する過程を示しています。特に、HTMLエスケープのロジックをコアの printer パッケージに直接組み込むことで、将来的な改善や機能拡張が容易になる基盤を築いたと言えます。

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

  • usr/gri/pretty/Makefile:
    • printer.6 の依存関係から htmlwriter.6 を削除。
  • usr/gri/pretty/htmlwriter.go:
    • ファイル全体が削除。
  • usr/gri/pretty/printer.go:
    • import "htmlwriter"; の削除。
    • type Printer struct から tags *htmlwriter.Writer; フィールドの削除。
    • func (P *Printer) Init(...) のシグネチャ変更(tags *htmlwriter.Writer 引数の削除)。
    • 新関数 func HtmlEscape(s string) string の追加。
    • func (P *Printer) String(pos int, s string)func (P *Printer) TaggedString(pos int, tag, s, endtag string) に変更され、旧 StringTaggedString を呼び出すラッパーに。
    • Printf やコメント出力部分で HtmlEscape の使用を開始。
    • HtmlPrologue, HtmlEpilogue, HtmlIdentifier メソッドの内部実装が P.tags.Tag から P.TaggedString を使用するように変更。
    • Print 関数内で htmlwriter.New の呼び出しを削除し、P.Init の引数を変更。
  • usr/gri/pretty/test.sh:
    • スキップ対象ファイルリストに types_amd64_darwin.go を追加。

コアとなるコードの解説

usr/gri/pretty/Makefile の変更

--- a/usr/gri/pretty/Makefile
+++ b/usr/gri/pretty/Makefile
@@ -37,7 +37,7 @@ parser.6:	 scanner.6 ast.6
 
 platform.6:	 utils.6
 
-printer.6:	 scanner.6 ast.6 htmlwriter.6
+printer.6:	 scanner.6 ast.6
 
 %.6:	%.go
 	$(G) $(F) $<

printer.6 のビルドに必要な依存関係から htmlwriter.6 が削除されました。これは、printer パッケージが htmlwriter パッケージに依存しなくなったことをビルドシステムに伝えています。これにより、htmlwriter.go ファイルの削除と整合性が取れます。

usr/gri/pretty/htmlwriter.go の削除

--- a/usr/gri/pretty/htmlwriter.go
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright 2009 The Go Authors.  All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package htmlwriter
-
-import (
-	"os";
-	"io";
-	"fmt";
-)
-
-// Writer is a filter implementing the io.Write interface.
-// It provides facilities to generate HTML tags and does
-// HTML-escaping for text written through Write. Incoming
-// text is assumed to be UTF-8 encoded.
-
-export type Writer struct {
-	// TODO should not export any of the fields
-	writer io.Write;
-}
-
-
-func (b *Writer) Init(writer io.Write) *Writer {
-	b.writer = writer;
-	return b;
-}
-
-
-/* export */ func (p *Writer) Write(buf *[]byte) (written int, err *os.Error) {
-	i0 := 0;
-	for i := i0; i < len(buf); i++ {
-		var s string;
-		switch buf[i] {
-		case '<': s = "&lt;";
-		case '&': s = "&amp;";
-		default: continue;
-		}
-		// write HTML escape instead of buf[i]
-		w1, e1 := p.writer.Write(buf[i0 : i]);
-		if e1 != nil {
-			return i0 + w1, e1;
-		}
-		w2, e2 := io.WriteString(p.writer, s);
-		if e2 != nil {
-			return i0 + w1 /* not w2! */, e2;
-		}
-		i0 = i + 1;
-	}
-	written, err = p.writer.Write(buf[i0 : len(buf)]);
-	return len(buf), err;
-}
-
-
-// ----------------------------------------------------------------------------
-// HTML-specific interface
-
-/* export */ func (p *Writer) Tag(s string) {
-	// TODO proper error handling
-	io.WriteString(p.writer, s);
-}
-
-
-// ----------------------------------------------------------------------------
-//
-
-export func New(writer io.Write) *Writer {
-	return new(Writer).Init(writer);
-}

htmlwriter.go は、HTMLエスケープとタグ生成の機能を提供していた独立したパッケージでした。このファイルの削除は、これらの機能が printer パッケージに直接統合されたことを明確に示しています。これにより、コードベースのモジュール構成が変更され、HTML出力に関するロジックがより一元化されました。

usr/gri/pretty/printer.go の変更

インポートと構造体の変更

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -11,7 +11,6 @@ import (
 	"tabwriter";
 	"flag";
 	"fmt";
-	"htmlwriter";
 	Scanner "scanner";
 	AST "ast";
 )
@@ -57,7 +56,6 @@ const (
 type Printer struct {
 	// output
 	text io.Write;
-	tags *htmlwriter.Writer;
 	
 	// comments
 	comments *array.Array;  // the list of all comments
@@ -94,10 +92,9 @@ func (P *Printer) NextComment() {
 }
 
 
-func (P *Printer) Init(text io.Write, tags *htmlwriter.Writer, comments *array.Array) {
+func (P *Printer) Init(text io.Write, comments *array.Array) {
 	// writers
 	P.text = text;
-	P.tags = tags;
 	
 	// comments
 	P.comments = comments;

htmlwriter パッケージのインポートが削除され、Printer 構造体から tags *htmlwriter.Writer フィールドが削除されました。これに伴い、Printer.Init メソッドのシグネチャも変更され、htmlwriter.Writer のインスタンスを受け取らなくなりました。これは、Printer がHTMLエスケープとタグ生成の責任を直接負うようになったことを示しています。

HtmlEscape 関数の追加

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -111,6 +108,22 @@ func (P *Printer) Init(text io.Write, tags *htmlwriter.Writer, comments *array.A
 // ----------------------------------------------------------------------------
 // Printing support
 
+func HtmlEscape(s string) string {
+	if html.BVal() {
+		var esc string;
+		for i := 0; i < len(s); i++ {
+			switch s[i] {
+			case '<': esc = "&lt";
+			case '&': esc = "&amp";
+			default: continue;
+			}
+			return s[0 : i] + esc + HtmlEscape(s[i+1 : len(s)]);
+		}
+	}
+	return s;
+}
+
+
 func (P *Printer) Printf(format string, s ...) {
 	n, err := fmt.fprintf(P.text, format, s);
 	if err != nil {

HtmlEscape 関数が追加されました。この関数は、HTML出力モード (html.BVal()) が有効な場合に、入力文字列 s 内の <& をそれぞれ &lt;&amp; に変換します。再帰的な実装により、文字列全体を走査し、特殊文字をエスケープします。この関数が printer パッケージ内に直接定義されたことで、HTMLエスケープ処理がより密接にコード整形ロジックと連携するようになりました。

String および TaggedString メソッドの変更

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -135,7 +148,7 @@ func (P *Printer) Newline(n int) {
 }\n \n \n-func (P *Printer) String(pos int, s string) {\n+func (P *Printer) TaggedString(pos int, tag, s, endtag string) {\n \t// use estimate for pos if we\'t have one\n \tif pos == 0 {\n \t\tpos = P.lastpos;\n@@ -230,7 +243,7 @@ func (P *Printer) String(pos int, s string) {\n \t\t\tif debug.BVal() {\n \t\t\t\tP.Printf(\"[%d]\", P.cpos);\n \t\t\t}\n-\t\t\tP.Printf(\"%s\", ctext);\n+\t\t\tP.Printf(\"%s\", HtmlEscape(ctext));\n \n \t\t\tif ctext[1] == \'/\' {\n \t\t\t\t//-style comments must end in newline\n@@ -276,7 +289,7 @@ func (P *Printer) String(pos int, s string) {\n \tif debug.BVal() {\n \t\tP.Printf(\"[%d]\", pos);\n \t}\n-\tP.Printf(\"%s\", s);\n+\tP.Printf(\"%s%s%s\", tag, HtmlEscape(s), endtag);\n \n \t// --------------------------------\n \t// interpret state\n@@ -300,6 +313,11 @@ func (P *Printer) String(pos int, s string) {\n }\n \n \n+func (P *Printer) String(pos int, s string) {\n+\tP.TaggedString(pos, \"\", s, \"\");\n+}\n+\n+\n func (P *Printer) Token(pos int, tok int) {\n \tP.String(pos, Scanner.TokenString(tok));\n }\n```
`String` メソッドが `TaggedString` にリネームされ、開始タグ、文字列、終了タグを受け取るようになりました。これにより、HTMLタグで囲まれたコンテンツをより柔軟に出力できるようになります。元の `String` メソッドは、新しい `TaggedString` を呼び出すラッパーとして再定義され、既存のコードとの互換性を保っています。コメントのテキスト (`ctext`) や通常の文字列出力も `HtmlEscape` を通すようになり、HTML出力時の安全性が向上しました。

#### HTML関連メソッドの変更

```diff
--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -316,36 +334,39 @@ func (P *Printer) Error(pos int, tok int, msg string) {\n // HTML support\n \n func (P *Printer) HtmlPrologue(title string) {\n-\tif P.tags != nil {\n-\t\tP.tags.Tag(\n+\tif html.BVal() {\n+\t\tP.TaggedString(0,\n \t\t\t"<html>\\n"\n \t\t\t"<head>\\n"\n \t\t\t"\t<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\\n"\n-\t\t\t"\t<title>" + title + "</title>\\n"\n+\t\t\t"\t<title>" + HtmlEscape(title) + "</title>\\n"\n \t\t\t"\t<style type=\"text/css\">\\n"\n \t\t\t"\t</style>\\n"\n \t\t\t"</head>\\n"\n \t\t\t"<body>\\n"\n-\t\t\t"<pre>\\n"\n+\t\t\t"<pre>\\n",\n+\t\t\t"", ""\n \t\t)\n \t}\n }\n \n \n func (P *Printer) HtmlEpilogue() {\n-\tif P.tags != nil {\n-\t\tP.tags.Tag(\n+\tif html.BVal() {\n+\t\tP.TaggedString(0, \n \t\t\t"</pre>\\n"\n \t\t\t"</body>\\n"\n-\t\t\t"<html>\\n"\n+\t\t\t"<html>\\n",\n+\t\t\t"", ""\n \t\t)\n \t}\n }\n \n \n func (P *Printer) HtmlIdentifier(pos int, ident string) {\n-\tif false && P.tags != nil {\n-\t\tP.tags.Tag(`<a href="#` + ident + `">` + ident + `</a>`);\n+\tif html.BVal() {\n+\t\t// no need to HtmlEscape ident\n+\t\tP.TaggedString(pos, `<a href="#` + ident + `">`, ident, `</a>`);\n \t} else {\n \t\tP.String(pos, ident);\n \t}\n```
`HtmlPrologue`, `HtmlEpilogue`, `HtmlIdentifier` の各メソッドも、`P.tags.Tag` の代わりに新しく導入された `P.TaggedString` を使用するように変更されました。これにより、HTMLタグの生成が `printer` パッケージ内で一貫して処理されるようになりました。特に、`HtmlPrologue` ではタイトルも `HtmlEscape` されるようになり、セキュリティが向上しています。

#### `Print` 関数の変更

```diff
--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -838,14 +859,8 @@ export func Print(prog *AST.Program) {\n \tif usetabs.BVal() {\n \t\tpadchar = '\t';\n \t}\n-\tvar (\n-\t\ttext = tabwriter.New(os.Stdout, int(tabwidth.IVal()), 1, padchar, true, html.BVal());\n-\t\ttags *htmlwriter.Writer;\n-\t)\n-\tif html.BVal() {\n-\t\ttags = htmlwriter.New(text);\n-\t}\n-\tP.Init(text, tags, prog.comments);\n+\ttext := tabwriter.New(os.Stdout, int(tabwidth.IVal()), 1, padchar, true, html.BVal());\n+\tP.Init(text, prog.comments);\n \n \tP.HtmlPrologue("<the source>");\n \tP.Program(prog);\

Print 関数は、pretty ツールのエントリポイントの一つです。この変更では、htmlwriter.New を呼び出して tags インスタンスを生成するロジックが完全に削除されました。これは、htmlwriter パッケージが不要になったことを最終的に示しています。P.Init の呼び出しも、tags 引数が削除された新しいシグネチャに合わせて更新されています。

usr/gri/pretty/test.sh の変更

--- a/usr/gri/pretty/test.sh
+++ b/usr/gri/pretty/test.sh
@@ -23,7 +23,7 @@ apply1() {\n 	#echo $1 $2\n 	case `basename $F` in\n 	# these files don\'t pass the idempotency test yet\n-\tlog.go | type.go | \\\n+\tlog.go | type.go | types_amd64_darwin.go | \\\
 \t\\\
 \tselftest1.go | func3.go | bug014.go | bug029.go | bug032.go | bug050.go | \\\
 \tbug068.go | bug088.go | bug083.go | bug106.go | bug125.go ) ;;  # skip - files contain syntax errors

test.sh スクリプトは、pretty ツールのテストを実行するためのものです。この変更では、types_amd64_darwin.go が、まだ冪等性テスト(整形しても結果が変わらないこと)をパスしないファイルとしてスキップリストに追加されました。これは、この特定のファイルが、新しいHTML出力ロジックや整形ロジックの変更によって、期待通りの結果を生成していなかった可能性を示唆しています。

関連リンク

  • Go言語公式サイト: https://go.dev/
  • Go言語の初期の歴史に関する情報(非公式リソースを含む)

参考にした情報源リンク

  • Go言語の公式ドキュメント(現在のバージョン)
  • HTMLエスケープに関する一般的な情報(OWASPなど)
  • Gitのコミットログと差分表示の解釈に関する知識
  • Makefile の基本的な構文と使用法
  • Go言語の io パッケージと text/tabwriter パッケージのドキュメント(当時のバージョンに相当する概念)
  • Go言語の初期開発に関するコミュニティの議論やブログ記事(もしあれば)